-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from all commits
f81891e
9a3d9e8
17e25ac
d69a46b
fca5cc7
993e1c3
4d4e09b
c4214a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
}; | ||
} | ||
} | ||
} |
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 | ||
{ | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the As I mentioned on Erik's PR, it would be great if soon we could have the possibility of registering alternative Oracle Groups. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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()); | ||
} | ||
} | ||
} |
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 } | ||
}; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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 beOracleContractAsyncFixed
, something like that, until we allow other to participate.