Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another Async Oracle Implementation #1743

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/neo/Ledger/Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/neo/Network/P2P/Payloads/InventoryType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ public enum InventoryType : byte
{
TX = MessageCommand.Transaction,
Block = MessageCommand.Block,
Consensus = MessageCommand.Consensus
Consensus = MessageCommand.Consensus,
}
}
60 changes: 60 additions & 0 deletions src/neo/Network/P2P/Payloads/OracleResponseAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
};
}
}
}
14 changes: 14 additions & 0 deletions src/neo/Network/P2P/Payloads/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<OracleResponseAttribute>().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;
}
Expand Down
5 changes: 4 additions & 1 deletion src/neo/Network/P2P/Payloads/TransactionAttributeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions src/neo/SmartContract/Native/NativeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
224 changes: 224 additions & 0 deletions src/neo/SmartContract/Native/Oracle/OracleContract.cs
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OracleContractAsync, if it is just for Committee and not open for other to register maybe it should be OracleContractAsyncFixed, something like that, until we allow other to participate.

{
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<ContractEventDescriptor>(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<ECPoint>();
UInt160 committeeAddress = NEO.GetCommitteeAddress(engine.Snapshot);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the 21 from the committee will be default oracles?

As I mentioned on Erik's PR, it would be great if soon we could have the possibility of registering alternative Oracle Groups.
The Default of any TX can still be the Commiteee, but there could be a field for choosing any other service. This looks quite crucial in my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

committeeAddress is used to check witness and committee will not be default oracles.Oracle nodes is designated by committee.

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<ECPoint>();
}

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<OracleRequest>();
}

[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<OracleResponseAttribute>().First();
}

private bool Response(ApplicationEngine engine, UInt256 responseTxHash, OracleResponseAttribute response)
{
OracleRequest request = engine.Snapshot.Storages.TryGet(CreateRequestKey(response.RequestTxHash))?.GetInteroperable<OracleRequest>();
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<OracleResponseAttribute>().FirstOrDefault();
if (response is null) throw new InvalidOperationException();
StorageKey requestKey = CreateRequestKey(response.RequestTxHash);
OracleRequest request = engine.Snapshot.Storages.GetAndChange(requestKey)?.GetInteroperable<OracleRequest>();
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<OracleResponseAttribute>().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<OracleRequest>();
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());
}
}
}
47 changes: 47 additions & 0 deletions src/neo/SmartContract/Native/Oracle/OracleRequest.cs
Original file line number Diff line number Diff line change
@@ -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<UInt256>();
Url = ((Struct)stackItem)[1].GetString();
FilterPath = @struct[2].GetString();
CallbackContract = @struct[3].GetSpan().AsSerializable<UInt160>();
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 }
};
}
}
}
Loading