diff --git a/src/neo/Ledger/Blockchain.cs b/src/neo/Ledger/Blockchain.cs index 50cee9509d..fb2fbdc78e 100644 --- a/src/neo/Ledger/Blockchain.cs +++ b/src/neo/Ledger/Blockchain.cs @@ -86,8 +86,7 @@ public static Blockchain Singleton static Blockchain() { GenesisBlock.RebuildMerkleRoot(); - - NativeContract[] contracts = { NativeContract.GAS, NativeContract.NEO }; + NativeContract[] contracts = { NativeContract.GAS, NativeContract.NEO, NativeContract.Oracle }; using (ScriptBuilder sb = new ScriptBuilder()) { foreach (NativeContract contract in contracts) diff --git a/src/neo/Network/P2P/Payloads/InventoryType.cs b/src/neo/Network/P2P/Payloads/InventoryType.cs index 0a1b831d12..a68e773f10 100644 --- a/src/neo/Network/P2P/Payloads/InventoryType.cs +++ b/src/neo/Network/P2P/Payloads/InventoryType.cs @@ -4,6 +4,6 @@ public enum InventoryType : byte { TX = MessageCommand.Transaction, Block = MessageCommand.Block, - Consensus = MessageCommand.Consensus + Consensus = MessageCommand.Consensus, } } diff --git a/src/neo/Network/P2P/Payloads/OracleResponseAttribute.cs b/src/neo/Network/P2P/Payloads/OracleResponseAttribute.cs new file mode 100644 index 0000000000..cd886ceaa4 --- /dev/null +++ b/src/neo/Network/P2P/Payloads/OracleResponseAttribute.cs @@ -0,0 +1,60 @@ +using Neo.IO; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.IO; + +namespace Neo.Network.P2P.Payloads +{ + public class OracleResponseAttribute : TransactionAttribute, IInteroperable + { + public UInt256 RequestTxHash; + public long FilterCost; + public byte[] Data; + + public override int Size => + base.Size + // Base size + UInt256.Length + // Request tx hash + sizeof(long) + // Filter cost + 1 + // Data type, 0x01 means normal data, 0x00 means null + (Data is null ? 1 : Data.GetVarSize()); // Data + + public override TransactionAttributeType Type => TransactionAttributeType.OracleResponse; + + public override bool AllowMultiple => false; + + protected override void DeserializeWithoutType(BinaryReader reader) + { + RequestTxHash = new UInt256(reader.ReadBytes(UInt256.Length)); + Data = reader.ReadByte() == 0x01 ? reader.ReadVarBytes(ushort.MaxValue) : null; + FilterCost = reader.ReadInt64(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(RequestTxHash); + if (Data != null) + { + writer.Write((byte)0x01); + writer.WriteVarBytes(Data); + } + else + { + writer.Write((byte)0x00); + } + writer.Write(FilterCost); + } + + public void FromStackItem(StackItem stackItem) => throw new System.NotImplementedException(); + + public StackItem ToStackItem(ReferenceCounter referenceCounter) + { + return new Struct(referenceCounter) + { + RequestTxHash.ToArray(), + Data, + FilterCost + }; + } + } +} diff --git a/src/neo/Network/P2P/Payloads/Transaction.cs b/src/neo/Network/P2P/Payloads/Transaction.cs index 10591659ca..f231980915 100644 --- a/src/neo/Network/P2P/Payloads/Transaction.cs +++ b/src/neo/Network/P2P/Payloads/Transaction.cs @@ -5,6 +5,7 @@ using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Native; +using Neo.SmartContract.Native.Tokens; using Neo.VM; using Neo.VM.Types; using Neo.Wallets; @@ -290,6 +291,19 @@ public virtual VerifyResult Verify(StoreView snapshot, BigInteger totalSenderFee if (size > MaxTransactionSize) return VerifyResult.Invalid; long net_fee = NetworkFee - size * NativeContract.Policy.GetFeePerByte(snapshot); if (net_fee < 0) return VerifyResult.InsufficientFunds; + + var oracleResponse = attributes.OfType().FirstOrDefault(); + if (oracleResponse != null) + { + if (Sender != NativeContract.Oracle.Hash || oracleResponse.FilterCost < 0) + return VerifyResult.Invalid; + var request = NativeContract.Oracle.GetRequest(snapshot, oracleResponse.RequestTxHash); + if (request is null || request.Status != RequestStatusType.Request) + return VerifyResult.Invalid; + if (request.OracleFee < oracleResponse.FilterCost + NativeContract.Oracle.GetRequestBaseFee(snapshot) + NetworkFee + SystemFee) + return VerifyResult.Invalid; + } + if (!this.VerifyWitnesses(snapshot, net_fee)) return VerifyResult.Invalid; return VerifyResult.Succeed; } diff --git a/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs index f1e2d704da..49f6a0451e 100644 --- a/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs +++ b/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -5,6 +5,9 @@ namespace Neo.Network.P2P.Payloads public enum TransactionAttributeType : byte { [ReflectionCache(typeof(Cosigner))] - Cosigner = 0x01 + Cosigner = 0x01, + + [ReflectionCache(typeof(OracleResponseAttribute))] + OracleResponse = 0x02 } } diff --git a/src/neo/SmartContract/Native/NativeContract.cs b/src/neo/SmartContract/Native/NativeContract.cs index 4d649ac627..c5313c7373 100644 --- a/src/neo/SmartContract/Native/NativeContract.cs +++ b/src/neo/SmartContract/Native/NativeContract.cs @@ -24,6 +24,7 @@ public abstract class NativeContract public static NeoToken NEO { get; } = new NeoToken(); public static GasToken GAS { get; } = new GasToken(); public static PolicyContract Policy { get; } = new PolicyContract(); + public static OracleContract Oracle { get; } = new OracleContract(); [ContractMethod(0, CallFlags.None)] public abstract string Name { get; } diff --git a/src/neo/SmartContract/Native/Oracle/OracleContract.cs b/src/neo/SmartContract/Native/Oracle/OracleContract.cs new file mode 100644 index 0000000000..26c1535d34 --- /dev/null +++ b/src/neo/SmartContract/Native/Oracle/OracleContract.cs @@ -0,0 +1,224 @@ +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native.Tokens; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native +{ + public class OracleContract : NativeContract + { + public override string Name => "Oracle"; + public override int Id => -5; + + private const byte Prefix_Validator = 37; + private const byte Prefix_RequestBaseFee = 13; + private const byte Prefix_RequestMaxValidHeight = 33; + private const byte Prefix_Request = 21; + private const byte Prefix_Response = 27; + + private const long ResponseTxMinFee = 1000; + private string[] SupportedProtocol = new string[] { "http", "https" }; + + public OracleContract() + { + Manifest.Features = ContractFeatures.HasStorage | ContractFeatures.Payable; + var events = new List(Manifest.Abi.Events) + { + new ContractEventDescriptor() + { + Name = "Request", + Parameters = new ContractParameterDefinition[] + { + new ContractParameterDefinition() + { + Name = "request", + Type = ContractParameterType.InteropInterface + } + } + } + }; + Manifest.Abi.Events = events.ToArray(); + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public bool Verify(ApplicationEngine engine) + { + UInt160 oracleAddress = GetOracleMultiSigAddress(engine.Snapshot); + return engine.CheckWitnessInternal(oracleAddress); + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public bool SetOracleValidators(ApplicationEngine engine, byte[] data) + { + ECPoint[] validators = data.AsSerializableArray(); + UInt160 committeeAddress = NEO.GetCommitteeAddress(engine.Snapshot); + if (validators.Length == 0 || !engine.CheckWitnessInternal(committeeAddress)) return false; + var storageItem = engine.Snapshot.Storages.GetAndChange(CreateStorageKey(Prefix_Validator), () => new StorageItem()); + storageItem.Value = validators.ToByteArray(); + return true; + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public ECPoint[] GetOracleValidators(StoreView snapshot) + { + StorageKey key = CreateStorageKey(Prefix_Validator); + StorageItem item = snapshot.Storages.TryGet(key); + return item?.Value.AsSerializableArray(); + } + + public UInt160 GetOracleMultiSigAddress(StoreView snapshot) + { + ECPoint[] oracleValidators = GetOracleValidators(snapshot); + return Contract.CreateMultiSigContract(oracleValidators.Length - (oracleValidators.Length - 1) / 3, oracleValidators).ScriptHash; + } + + [ContractMethod(0_03000000, CallFlags.AllowModifyStates)] + public bool SetRequestBaseFee(ApplicationEngine engine, long requestBaseFee) + { + UInt160 account = NEO.GetCommitteeAddress(engine.Snapshot); + if (!engine.CheckWitnessInternal(account)) return false; + if (requestBaseFee <= 0) return false; + StorageItem storage = engine.Snapshot.Storages.GetAndChange(CreateStorageKey(Prefix_RequestBaseFee), () => new StorageItem()); + storage.Value = BitConverter.GetBytes(requestBaseFee); + return true; + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public long GetRequestBaseFee(StoreView snapshot) + { + StorageItem storage = snapshot.Storages.TryGet(CreateStorageKey(Prefix_RequestBaseFee)); + if (storage is null) return 0; + return BitConverter.ToInt64(storage.Value); + } + + [ContractMethod(0_03000000, CallFlags.AllowModifyStates)] + public bool SetRequestMaxValidHeight(ApplicationEngine engine, uint ValidHeight) + { + UInt160 committeeAddress = NEO.GetCommitteeAddress(engine.Snapshot); + if (!engine.CheckWitnessInternal(committeeAddress)) return false; + StorageItem storage = engine.Snapshot.Storages.GetAndChange(CreateStorageKey(Prefix_RequestMaxValidHeight), () => new StorageItem()); + storage.Value = BitConverter.GetBytes(ValidHeight); + return true; + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public uint GetRequestMaxValidHeight(StoreView snapshot) + { + StorageItem storage = snapshot.Storages.TryGet(CreateStorageKey(Prefix_RequestMaxValidHeight)); + if (storage is null) return 0; + return BitConverter.ToUInt32(storage.Value); + } + + [ContractMethod(0_01000000, CallFlags.All)] + public bool Request(ApplicationEngine engine, string url, string filterPath, string callbackMethod, long oracleFee) + { + Transaction tx = (Transaction)engine.GetScriptContainer(); + var requestKey = CreateRequestKey(tx.Hash); + if (engine.Snapshot.Storages.TryGet(requestKey) != null) throw new ArgumentException("One transaction can only request once"); + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) throw new ArgumentException("It's not a valid request"); + if (!SupportedProtocol.Contains(uri.Scheme.ToLowerInvariant())) throw new ArgumentException($"The scheme '{uri.Scheme}' is not allowed"); + if (oracleFee < GetRequestBaseFee(engine.Snapshot) + ResponseTxMinFee) throw new InvalidOperationException("OracleFee is not enough"); + + // OracleFee = RequestBaseFee + FilterCost + ResponseTxFee + // FilterCost = Size of the requested data * GasPerByte + // ResponseTxFee = ResponseTx.NetwrokFee + ResponseTx.SystemFee + + engine.AddGas(oracleFee); + GAS.Mint(engine, Hash, oracleFee - GetRequestBaseFee(engine.Snapshot)); // pay response tx + + OracleRequest request = new OracleRequest() + { + Url = url, + FilterPath = filterPath, + CallbackContract = engine.CallingScriptHash, + CallbackMethod = callbackMethod, + OracleFee = oracleFee, + RequestTxHash = tx.Hash, + ValidHeight = engine.GetBlockchainHeight() + GetRequestMaxValidHeight(engine.Snapshot), + Status = RequestStatusType.Request + }; + engine.Snapshot.Storages.Add(requestKey, new StorageItem(request)); + engine.SendNotification(Hash, "Request", new Array() { StackItem.FromInterface(request) }); + return true; + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public OracleRequest GetRequest(StoreView snapshot, UInt256 requestTxHash) + { + return snapshot.Storages.TryGet(CreateRequestKey(requestTxHash))?.GetInteroperable(); + } + + [ContractMethod(0_01000000, CallFlags.AllowStates)] + public OracleResponseAttribute GetResponse(ApplicationEngine engine, UInt256 requestTxHash) + { + var item = engine.Snapshot.Storages.TryGet(CreateStorageKey(Prefix_Response, requestTxHash)); + if (item is null || item.Value is null) throw new ArgumentException("Response does not exist"); + var responseTxHash = new UInt256(item.Value); + return engine.Snapshot.Transactions.TryGet(responseTxHash).Transaction.Attributes.OfType().First(); + } + + private bool Response(ApplicationEngine engine, UInt256 responseTxHash, OracleResponseAttribute response) + { + OracleRequest request = engine.Snapshot.Storages.TryGet(CreateRequestKey(response.RequestTxHash))?.GetInteroperable(); + if (request is null || request.Status != RequestStatusType.Request || request.ValidHeight < engine.Snapshot.Height) return false; + request.Status = RequestStatusType.Ready; + engine.Snapshot.Storages.Add(CreateStorageKey(Prefix_Response, response.RequestTxHash), new StorageItem() { Value = responseTxHash.ToArray() }); + return true; + } + + [ContractMethod(0_01000000, CallFlags.All)] + public void Callback(ApplicationEngine engine) + { + UInt160 oracleAddress = GetOracleMultiSigAddress(engine.Snapshot); + if (!engine.CheckWitnessInternal(oracleAddress)) throw new InvalidOperationException(); + Transaction tx = (Transaction)engine.ScriptContainer; + if (tx is null) throw new InvalidOperationException(); + OracleResponseAttribute response = tx.Attributes.OfType().FirstOrDefault(); + if (response is null) throw new InvalidOperationException(); + StorageKey requestKey = CreateRequestKey(response.RequestTxHash); + OracleRequest request = engine.Snapshot.Storages.GetAndChange(requestKey)?.GetInteroperable(); + if (request is null || request.Status != RequestStatusType.Ready) throw new InvalidOperationException(); + + engine.CallFromNativeContract(() => + { + request.Status = RequestStatusType.Successed; + }, request.CallbackContract, request.CallbackMethod, response.Data); + } + + protected override void OnPersist(ApplicationEngine engine) + { + base.OnPersist(engine); + foreach (Transaction tx in engine.Snapshot.PersistingBlock.Transactions) + { + OracleResponseAttribute response = tx.Attributes.OfType().FirstOrDefault(); + if (response is null) continue; + if (Response(engine, tx.Hash, response)) + { + UInt160[] oracleNodes = GetOracleValidators(engine.Snapshot).Select(p => Contract.CreateSignatureContract(p).ScriptHash).ToArray(); + long nodeReward = (response.FilterCost + GetRequestBaseFee(engine.Snapshot)) / oracleNodes.Length; + foreach (UInt160 account in oracleNodes) + GAS.Mint(engine, account, nodeReward); + + OracleRequest request = engine.Snapshot.Storages.TryGet(CreateRequestKey(response.RequestTxHash))?.GetInteroperable(); + long refund = request.OracleFee - response.FilterCost - GetRequestBaseFee(engine.Snapshot) - tx.NetworkFee - tx.SystemFee; + Transaction requestTx = engine.Snapshot.Transactions.TryGet(request.RequestTxHash).Transaction; + GAS.Mint(engine, requestTx.Sender, refund); + GAS.Burn(engine, Hash, refund + response.FilterCost); + } + } + } + + private StorageKey CreateRequestKey(UInt256 requestTxHash) + { + return CreateStorageKey(Prefix_Request, requestTxHash.ToArray()); + } + } +} diff --git a/src/neo/SmartContract/Native/Oracle/OracleRequest.cs b/src/neo/SmartContract/Native/Oracle/OracleRequest.cs new file mode 100644 index 0000000000..b22e3e6aed --- /dev/null +++ b/src/neo/SmartContract/Native/Oracle/OracleRequest.cs @@ -0,0 +1,47 @@ +using Neo.IO; +using Neo.VM; +using Neo.VM.Types; +using System.IO; + +namespace Neo.SmartContract.Native.Tokens +{ + public class OracleRequest : IInteroperable + { + public UInt256 RequestTxHash; + public string Url; + public string FilterPath; + public UInt160 CallbackContract; + public string CallbackMethod; + public uint ValidHeight; + public long OracleFee; + public RequestStatusType Status; + + public virtual void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + RequestTxHash = @struct[0].GetSpan().AsSerializable(); + Url = ((Struct)stackItem)[1].GetString(); + FilterPath = @struct[2].GetString(); + CallbackContract = @struct[3].GetSpan().AsSerializable(); + CallbackMethod = @struct[4].GetString(); + ValidHeight = (uint)@struct[5].GetInteger(); + OracleFee = (long)@struct[6].GetInteger(); + Status = (RequestStatusType)@struct[7].GetSpan().ToArray()[0]; + } + + public virtual StackItem ToStackItem(ReferenceCounter referenceCounter) + { + return new Struct(referenceCounter) + { + RequestTxHash.ToArray(), + Url, + FilterPath, + CallbackContract.ToArray(), + CallbackMethod, + ValidHeight, + OracleFee, + new byte[]{ (byte)Status } + }; + } + } +} diff --git a/src/neo/SmartContract/Native/Oracle/RequestStatusType.cs b/src/neo/SmartContract/Native/Oracle/RequestStatusType.cs new file mode 100644 index 0000000000..620809c79d --- /dev/null +++ b/src/neo/SmartContract/Native/Oracle/RequestStatusType.cs @@ -0,0 +1,10 @@ +namespace Neo.SmartContract.Native.Tokens +{ + public enum RequestStatusType : byte + { + Request = 0x00, + Ready = 0x01, + Successed = 0x02, + Failed = 0x03 + } +} diff --git a/tests/neo.UnitTests/SmartContract/Native/Oracle/UT_OracleContract.cs b/tests/neo.UnitTests/SmartContract/Native/Oracle/UT_OracleContract.cs new file mode 100644 index 0000000000..c72f98b178 --- /dev/null +++ b/tests/neo.UnitTests/SmartContract/Native/Oracle/UT_OracleContract.cs @@ -0,0 +1,404 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.SmartContract.Native.Tokens; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using static Neo.UnitTests.Extensions.Nep5NativeContractExtensions; + +namespace Neo.UnitTests.SmartContract.Native +{ + [TestClass] + public class UT_OracleContract + { + [TestInitialize] + public void TestSetup() + { + TestBlockchain.InitializeMockNeoSystem(); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[] key = null) + { + StorageKey storageKey = new StorageKey + { + Id = NativeContract.Oracle.Id, + Key = new byte[sizeof(byte) + (key?.Length ?? 0)] + }; + storageKey.Key[0] = prefix; + key?.CopyTo(storageKey.Key.AsSpan(1)); + return storageKey; + } + + [TestMethod] + public void Test_GetPerRequestFee() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "getRequestBaseFee"); + var engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Integer)); + Assert.AreEqual(result, 0); + } + + [TestMethod] + public void Test_SetPerRequestFee() + { + var snapshot = Blockchain.Singleton.GetSnapshot().Clone(); + + // Init + var engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + var from = NativeContract.NEO.GetCommitteeAddress(engine.Snapshot); + long value = 12345; + + // Set + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setRequestBaseFee", value); + engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(from), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + Assert.IsTrue((result as VM.Types.Boolean).GetBoolean()); + + // Set (wrong witness) + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setRequestBaseFee", value); + engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(null), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + Assert.IsFalse((result as VM.Types.Boolean).GetBoolean()); + + // Set wrong (negative) + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setRequestBaseFee", -1); + engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(from), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + Assert.IsFalse((result as VM.Types.Boolean).GetBoolean()); + + // Get + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "getRequestBaseFee"); + engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Integer)); + Assert.AreEqual(result, value); + } + + [TestMethod] + public void Test_GetValidHeight() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "getRequestMaxValidHeight"); + var engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Integer)); + Assert.AreEqual(result, 0); + } + + [TestMethod] + public void Test_SetValidHeight() + { + var snapshot = Blockchain.Singleton.GetSnapshot().Clone(); + + // Init + var engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + var from = NativeContract.NEO.GetCommitteeAddress(snapshot); + uint value = 123; + + // Set + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setRequestMaxValidHeight", value); + engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(from), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + Assert.IsTrue((result as VM.Types.Boolean).GetBoolean()); + + // Set (wrong witness) + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setRequestMaxValidHeight", value); + engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(null), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + Assert.IsFalse((result as VM.Types.Boolean).GetBoolean()); + + // Get + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "getRequestMaxValidHeight"); + engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Integer)); + Assert.AreEqual(result, value); + } + + [TestMethod] + public void Test_GetOracleValidators() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + + var from = NativeContract.NEO.GetCommitteeAddress(snapshot); + Neo.Cryptography.ECC.ECPoint[] defaultNodes = NativeContract.NEO.GetCommittee(snapshot); + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setOracleValidators", defaultNodes.ToByteArray()); + var engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(from), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + engine.Execute(); + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "getOracleValidators"); + engine = new ApplicationEngine(TriggerType.Application, null, snapshot, 0, true); + engine.LoadScript(script.ToArray()); + + engine.Execute().Should().Be(VMState.HALT); + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Array)); + Assert.AreEqual(21, ((VM.Types.Array)result).Count); + } + + [TestMethod] + public void Check_Request() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var request = new OracleRequest() + { + Url = "https://www.baidu.com/", + FilterPath = "dotest", + OracleFee = 10000 + }; + var ret_Request = Check_Request(snapshot, request, out UInt256 requestTxHash, out Transaction tx); + ret_Request.Result.GetBoolean().Should().BeTrue(); + ret_Request.State.Should().BeTrue(); + } + + internal static (bool State, StackItem Result) Check_Request(StoreView snapshot, OracleRequest request, out UInt256 requestTxHash, out Transaction tx) + { + var from = NativeContract.NEO.GetCommitteeAddress(snapshot); + Neo.Cryptography.ECC.ECPoint[] defaultNodes = NativeContract.NEO.GetCommittee(snapshot); + var script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, "setOracleValidators", defaultNodes.ToByteArray()); + var engine = new ApplicationEngine(TriggerType.Application, new ManualWitness(from), snapshot, 0, true); + engine.LoadScript(script.ToArray()); + engine.Execute(); + + snapshot.PersistingBlock = new Block() { Index = 1000 }; + byte[] privateKey = new byte[32]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(privateKey); + } + KeyPair keyPair = new KeyPair(privateKey); + UInt160 account = Contract.CreateSignatureRedeemScript(keyPair.PublicKey).ToScriptHash(); + + script = new ScriptBuilder(); + script.EmitAppCall(NativeContract.Oracle.Hash, + "request", + request.Url, + request.FilterPath, + "back", + request.OracleFee); + script.Emit(OpCode.RET); + script.Emit(OpCode.DROP); + script.Emit(OpCode.RET); + ContractManifest manifest = new ContractManifest() + { + Permissions = new[] { ContractPermission.DefaultPermission }, + Abi = new ContractAbi() + { + Hash = script.ToArray().ToScriptHash(), + Events = new ContractEventDescriptor[0], + Methods = new ContractMethodDescriptor[0] + }, + Features = ContractFeatures.NoProperty, + Groups = new ContractGroup[0], + SafeMethods = WildcardContainer.Create(), + Trusts = WildcardContainer.Create(), + Extra = null, + }; + manifest.Abi.Methods = new ContractMethodDescriptor[] + { + new ContractMethodDescriptor() + { + Name = "testInvoke", + Parameters = new ContractParameterDefinition[0], + ReturnType = ContractParameterType.Void, + Offset=0x00 + }, + new ContractMethodDescriptor() + { + Name = "back", + Parameters =new ContractParameterDefinition[]{ + new ContractParameterDefinition(){ + Name="data", + Type=ContractParameterType.ByteArray + } + }, + ReturnType = ContractParameterType.Void, + Offset=script.ToArray().Length-2 + } + }; + ContractState contractState = new ContractState + { + Id = 0x43000000, + Script = script.ToArray(), + Manifest = manifest + }; + snapshot.Contracts.Add(contractState.ScriptHash, contractState); + + ScriptBuilder builder = new ScriptBuilder(); + builder.EmitAppCall(contractState.ScriptHash, "testInvoke"); + tx = new Transaction + { + Version = 0, + Nonce = (uint)1000, + Script = builder.ToArray(), + Sender = account, + ValidUntilBlock = snapshot.Height + Transaction.MaxValidUntilBlockIncrement, + Attributes = new TransactionAttribute[0], + Witnesses = new Witness[] { new Witness + { + InvocationScript = System.Array.Empty(), + VerificationScript = Contract.CreateSignatureRedeemScript(keyPair.PublicKey) + }} + }; + var data = new ContractParametersContext(tx); + byte[] sig = data.Verifiable.Sign(keyPair); + tx.Witnesses[0].InvocationScript = sig; + requestTxHash = tx.Hash; + engine = ApplicationEngine.Run(builder.ToArray(), snapshot, tx, null, 0, true); + if (engine.State == VMState.FAULT) + { + return (false, false); + } + + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + return (true, result); + } + + [TestMethod] + public void Check_CallBack() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var request = new OracleRequest() + { + Url = "https://www.baidu.com/", + FilterPath = "dotest", + CallbackMethod = "back", + OracleFee = 1000L + }; + var ret_Request = Check_Request(snapshot, request, out UInt256 requestTxHash, out Transaction tx); + ret_Request.Result.GetBoolean().Should().Be(true); + ret_Request.State.Should().BeTrue(); + snapshot.Transactions.Add(tx.Hash, new TransactionState() { Transaction = tx, VMState = VMState.HALT, BlockIndex = snapshot.PersistingBlock.Index }); + + byte[] privateKey = new byte[32]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(privateKey); + } + KeyPair keyPair = new KeyPair(privateKey); + + OracleResponseAttribute response = new OracleResponseAttribute(); + response.RequestTxHash = requestTxHash; + response.Data = keyPair.PublicKey.ToArray(); + response.FilterCost = 0; + Transaction responsetx = CreateResponseTransaction(snapshot, response); + Console.WriteLine(responsetx.SystemFee); + } + + private static Transaction CreateResponseTransaction(StoreView initsnapshot, OracleResponseAttribute response) + { + StoreView snapshot = initsnapshot.Clone(); + + var oracleAddress = NativeContract.Oracle.GetOracleMultiSigAddress(snapshot); + ScriptBuilder sb = new ScriptBuilder(); + sb.EmitAppCall(NativeContract.Oracle.Hash, "onPersist"); + + var tx = new Transaction() + { + Version = 0, + ValidUntilBlock = snapshot.Height + Transaction.MaxValidUntilBlockIncrement, + Attributes = new TransactionAttribute[]{ + new Cosigner() + { + Account = oracleAddress, + AllowedContracts = new UInt160[]{ NativeContract.Oracle.Hash }, + Scopes = WitnessScope.CustomContracts + }, + response + }, + Sender = oracleAddress, + Witnesses = new Witness[0], + Script = sb.ToArray(), + NetworkFee = 0, + Nonce = 0, + SystemFee = 0 + }; + + snapshot.PersistingBlock = new Block() { Index = snapshot.Height + 1, Transactions = new Transaction[] { tx } }; + //commit response + var engine = new ApplicationEngine(TriggerType.System, null, snapshot, 0, true); + engine.LoadScript(sb.ToArray()); + if (engine.Execute() != VMState.HALT) throw new InvalidOperationException(); + + var sb2 = new ScriptBuilder(); + sb2.EmitAppCall(NativeContract.Oracle.Hash, "callback"); + + var state = new TransactionState + { + BlockIndex = snapshot.PersistingBlock.Index, + Transaction = tx + }; + snapshot.Transactions.Add(tx.Hash, state); + + var engine2 = ApplicationEngine.Run(sb2.ToArray(), snapshot, tx, testMode: true); + if (engine2.State != VMState.HALT) throw new ApplicationException(); + tx.SystemFee = engine2.GasConsumed; + // Calculate network fee + int size = tx.Size; + var oracleValidators = NativeContract.Oracle.GetOracleValidators(snapshot); + var oracleMultiContract = Contract.CreateMultiSigContract(oracleValidators.Length - (oracleValidators.Length - 1) / 3, oracleValidators); + tx.NetworkFee += Wallet.CalculateNetworkFee(oracleMultiContract.Script, ref size); + tx.NetworkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); + return tx; + } + + } +} diff --git a/tests/neo.UnitTests/neo.UnitTests.csproj b/tests/neo.UnitTests/neo.UnitTests.csproj index c1d5a7200d..a588c7da5a 100644 --- a/tests/neo.UnitTests/neo.UnitTests.csproj +++ b/tests/neo.UnitTests/neo.UnitTests.csproj @@ -1,4 +1,4 @@ - + Exe