Skip to content

Commit

Permalink
Oracle: Allow wallets to add assert for the oracle response (#1541)
Browse files Browse the repository at this point in the history
* Add assert for each oracle request

* Change to goto

* Improve wallet asserts

* Optimize

* Fix URL type

* dotnet format

* Oracle: Tx comunication (#1540)

* Tx comunication

* Some fixes

* Optimize OracleExecutionCache

* Add assert for each oracle request

* Change to goto

* Improve wallet asserts

* Optimize

* Fix URL type

* dotnet format

* Optimize OracleExecutionCache

* MakeTransaction UT
  • Loading branch information
shargon committed Apr 6, 2020
1 parent 06d281c commit c101958
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 11 deletions.
47 changes: 43 additions & 4 deletions src/neo/Oracle/OracleExecutionCache.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Neo.IO;
using Neo.SmartContract;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -29,8 +30,42 @@ public class OracleExecutionCache : IEnumerable<KeyValuePair<UInt160, OracleResp
/// </summary>
public long FilterCost { get; private set; }

/// <summary>
/// Responses
/// </summary>
public OracleResponse[] Responses => _cache.Values.ToArray();

public int Size => IO.Helper.GetVarSize(Count) + _cache.Values.Sum(u => u.Size);

private UInt160 _hash;

/// <summary>
/// Hash
/// </summary>
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;
}
}

/// <summary>
/// Constructor for oracles
/// </summary>
Expand All @@ -45,6 +80,7 @@ public OracleExecutionCache(Func<OracleRequest, OracleResponse> oracle = null) :
/// </summary>
public OracleExecutionCache()
{
_hash = null;
FilterCost = 0;
}

Expand All @@ -54,9 +90,11 @@ public OracleExecutionCache()
/// <param name="results">Results</param>
public OracleExecutionCache(params OracleResponse[] results)
{
_oracle = null;
FilterCost = 0;

_hash = null;
_oracle = null;

foreach (var result in results)
{
_cache[result.RequestHash] = result;
Expand Down Expand Up @@ -109,12 +147,13 @@ public void Serialize(BinaryWriter writer)

public void Deserialize(BinaryReader reader)
{
var results = reader.ReadSerializableArray<OracleResponse>(byte.MaxValue);

FilterCost = 0;
_hash = null;

var entries = reader.ReadSerializableArray<OracleResponse>(byte.MaxValue);
_cache.Clear();

foreach (var result in results)
foreach (var result in entries)
{
_cache[result.RequestHash] = result;
FilterCost += result.FilterCost;
Expand Down
20 changes: 20 additions & 0 deletions src/neo/Oracle/OracleWalletBehaviour.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Neo.Oracle
{
public enum OracleWalletBehaviour
{
/// <summary>
/// If an Oracle syscall was found, the tx will fault
/// </summary>
WithoutOracle,

/// <summary>
/// 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)
/// </summary>
OracleWithoutAssert,

/// <summary>
/// If an Oracle syscall was found, it will be added an asert at the begining of the script
/// </summary>
OracleWithAssert
}
}
19 changes: 19 additions & 0 deletions src/neo/SmartContract/InteropService.Oracle.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Neo.IO;
using Neo.Network.P2P.Payloads;
using Neo.Oracle;
using Neo.Oracle.Protocols.Https;
Expand All @@ -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);

/// <summary>
/// Oracle get the hash of the current OracleFlow [Request/Response]
/// </summary>
private static bool Oracle_Hash(ApplicationEngine engine)
{
if (engine.OracleCache == null)
{
engine.Push(StackItem.Null);
}
else
{
engine.Push(engine.OracleCache.Hash.ToArray());
}

return true;
}

/// <summary>
/// Oracle Get
Expand Down
4 changes: 3 additions & 1 deletion src/neo/SmartContract/Native/Oracle/OracleContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OracleExecutionCache>();

// It should be cached by the ApplicationEngine so we can save space removing it
Expand Down
79 changes: 73 additions & 6 deletions src/neo/Wallets/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -294,27 +296,46 @@ 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<OracleRequest> oracleRequests = null;

if (oracle != OracleWalletBehaviour.WithoutOracle)
{
// Wee need the full request in order to duplicate the call for asserts

oracleRequests = new List<OracleRequest>();
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,
ValidUntilBlock = snapshot.Height + Transaction.MaxValidUntilBlockIncrement,
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()}'");
Expand All @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions tests/neo.UnitTests/Oracle/UT_OracleService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<InvalidOperationException>(() =>
{
_ = 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()
{
Expand Down

0 comments on commit c101958

Please sign in to comment.