Skip to content

Commit

Permalink
UtxoReferee Refactoring (#1448)
Browse files Browse the repository at this point in the history
UtxoReferee Refactoring
  • Loading branch information
nopara73 committed May 16, 2019
2 parents 189c072 + 9ba049d commit f547b27
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 76 deletions.
13 changes: 5 additions & 8 deletions WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.BouncyCastle.Math;
Expand Down Expand Up @@ -60,8 +60,7 @@ public static IEnumerable<CcjRunningRoundState> GetStatesCollection()

foreach (CcjRound round in Coordinator.GetRunningRounds())
{
var state = new CcjRunningRoundState
{
var state = new CcjRunningRoundState {
Phase = round.Phase,
SchnorrPubKeys = round.MixingLevels.SchnorrPubKeys,
Denomination = round.MixingLevels.GetBaseDenomination(),
Expand Down Expand Up @@ -180,7 +179,7 @@ public async Task<IActionResult> PostInputsAsync([FromBody, Required]InputsReque
var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo: false);
if (bannedElem != null)
{
return BadRequest($"Input is banned from participation for {(int)bannedElem.Value.bannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.");
return BadRequest($"Input is banned from participation for {(int)bannedElem.BannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.");
}

var txoutResponseTask = batch.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool: true);
Expand Down Expand Up @@ -328,8 +327,7 @@ public async Task<IActionResult> PostInputsAsync([FromBody, Required]InputsReque
}
}

var resp = new InputsResponse
{
var resp = new InputsResponse {
UniqueId = alice.UniqueId,
RoundId = round.RoundId
};
Expand Down Expand Up @@ -374,8 +372,7 @@ public async Task<IActionResult> PostConfirmationAsync([FromQuery, Required]stri
CcjRoundPhase phase = round.Phase;

// Start building the response.
var resp = new ConnConfResp
{
var resp = new ConnConfResp {
CurrentPhase = phase
};

Expand Down
38 changes: 19 additions & 19 deletions WalletWasabi.Tests/ModelTests.cs
@@ -1,4 +1,4 @@
using NBitcoin;
using NBitcoin;
using NBitcoin.BouncyCastle.Math;
using NBitcoin.RPC;
using Newtonsoft.Json;
Expand Down Expand Up @@ -116,26 +116,27 @@ public void SmartTransactionSerialization()
[Fact]
public void UtxoRefereeSerialization()
{
var record = UtxoReferee.BannedRecordFromLine("2018-11-23 15-23-14:1:44:2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492:True:195");
var record = BannedUtxoRecord.FromString("2018-11-23 15-23-14:1:44:2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492:True:195");

Assert.Equal(new DateTimeOffset(2018, 11, 23, 15, 23, 14, TimeSpan.Zero), record.timeOfBan);
Assert.Equal(1, record.severity);
Assert.Equal(44u, record.utxo.N);
Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record.utxo.Hash);
Assert.True(record.isNoted);
Assert.Equal(195, record.bannedForRound);
Assert.Equal(new DateTimeOffset(2018, 11, 23, 15, 23, 14, TimeSpan.Zero), record.TimeOfBan);
Assert.Equal(1, record.Severity);
Assert.Equal(44u, record.Utxo.N);
Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record.Utxo.Hash);
Assert.True(record.IsNoted);
Assert.Equal(195, record.BannedForRound);

DateTimeOffset dateTime = DateTimeOffset.UtcNow;
DateTimeOffset now = new DateTimeOffset(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), TimeSpan.Zero);
string record2Line = UtxoReferee.BannedRecordToLine(record.utxo, 3, now, false, 99);
var record2 = UtxoReferee.BannedRecordFromLine(record2Line);

Assert.Equal(now, record2.timeOfBan);
Assert.Equal(3, record2.severity);
Assert.Equal(44u, record2.utxo.N);
Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record2.utxo.Hash);
Assert.False(record2.isNoted);
Assert.Equal(99, record2.bannedForRound);
var record2Init = new BannedUtxoRecord(record.Utxo, 3, now, false, 99);
string record2Line = record2Init.ToString();
var record2 = BannedUtxoRecord.FromString(record2Line);

Assert.Equal(now, record2.TimeOfBan);
Assert.Equal(3, record2.Severity);
Assert.Equal(44u, record2.Utxo.N);
Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record2.Utxo.Hash);
Assert.False(record2.IsNoted);
Assert.Equal(99, record2.BannedForRound);
}

[Fact]
Expand Down Expand Up @@ -196,8 +197,7 @@ public void AllFeeEstimateSerialization()
[Fact]
public void InputsResponseSerialization()
{
var resp = new InputsResponse
{
var resp = new InputsResponse {
UniqueId = Guid.NewGuid(),
RoundId = 1,
};
Expand Down
52 changes: 52 additions & 0 deletions WalletWasabi/Models/BannedUtxoRecord.cs
@@ -0,0 +1,52 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WalletWasabi.Helpers;

namespace WalletWasabi.Models
{
public class BannedUtxoRecord
{
public OutPoint Utxo { get; }
public int Severity { get; }
public DateTimeOffset TimeOfBan { get; }
public bool IsNoted { get; }
public long BannedForRound { get; }
public TimeSpan BannedRemaining => DateTimeOffset.UtcNow - TimeOfBan;

public BannedUtxoRecord(OutPoint utxo, int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)
{
Utxo = Guard.NotNull(nameof(utxo), utxo);
Severity = severity;
TimeOfBan = timeOfBan;
IsNoted = isNoted;
BannedForRound = bannedForRound;
}

/// <summary>
/// Deserializes an instance from its text representation.
/// </summary>
public static BannedUtxoRecord FromString(string str)
{
var parts = str.Split(':');
var isNoted = bool.Parse(parts[4]);
var bannedForRound = long.Parse(parts[5]);
var utxo = new OutPoint(new uint256(parts[3]), int.Parse(parts[2]));
var severity = int.Parse(parts[1]);
var timeParts = parts[0].Split('-', ' ').Select(x => int.Parse(x)).ToArray();
var timeOfBan = new DateTimeOffset(timeParts[0], timeParts[1], timeParts[2], timeParts[3], timeParts[4], timeParts[5], TimeSpan.Zero);

return new BannedUtxoRecord(utxo, severity, timeOfBan, isNoted, bannedForRound);
}

/// <summary>
/// Serializes the instance to its text representation.
/// </summary>
public override string ToString()
{
return $"{TimeOfBan.ToString("yyyy-MM-dd HH-mm-ss")}:{Severity}:{Utxo.N}:{Utxo.Hash}:{IsNoted}:{BannedForRound}";
}
}
}
6 changes: 3 additions & 3 deletions WalletWasabi/Services/CcjCoordinator.cs
Expand Up @@ -181,13 +181,13 @@ public async Task ProcessTransactionAsync(Transaction tx)
{
if (!AnyRunningRoundContainsInput(prevOut, out _))
{
int newSeverity = foundElem.Value.severity + 1;
int newSeverity = foundElem.Severity + 1;
await UtxoReferee.UnbanAsync(prevOut); // since it's not an UTXO anymore

if (RoundConfig.DosSeverity >= newSeverity)
{
var txCoins = tx.Outputs.AsIndexedOutputs().Select(x => x.ToCoin().Outpoint);
await UtxoReferee.BanUtxosAsync(newSeverity, foundElem.Value.timeOfBan, forceNoted: foundElem.Value.isNoted, foundElem.Value.bannedForRound, txCoins.ToArray());
await UtxoReferee.BanUtxosAsync(newSeverity, foundElem.TimeOfBan, forceNoted: foundElem.IsNoted, foundElem.BannedForRound, txCoins.ToArray());
}
}
}
Expand Down Expand Up @@ -307,7 +307,7 @@ private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status
feePerInputs = fees.feePerInputs;
feePerOutputs = fees.feePerOutputs;

Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + 2 * feePerOutputs);
Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + (2 * feePerOutputs));
if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination)
{
if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
Expand Down
73 changes: 27 additions & 46 deletions WalletWasabi/Services/UtxoReferee.cs
@@ -1,4 +1,4 @@
using NBitcoin;
using NBitcoin;
using NBitcoin.RPC;
using System;
using System.Collections.Concurrent;
Expand All @@ -9,16 +9,14 @@
using System.Threading.Tasks;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
using WalletWasabi.Models;
using WalletWasabi.Models.ChaumianCoinJoin;

namespace WalletWasabi.Services
{
public class UtxoReferee
{
/// <summary>
/// Key: banned utxo, Value: severity level, time of ban, if it's only in noted status, which round it disrupted
/// </summary>
private ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)> BannedUtxos { get; }
private ConcurrentDictionary<OutPoint, BannedUtxoRecord> BannedUtxos { get; }

public string BannedUtxosFilePath => Path.Combine(FolderPath, $"BannedUtxos{Network}.txt");

Expand All @@ -36,7 +34,7 @@ public UtxoReferee(Network network, string folderPath, RPCClient rpc, CcjRoundCo
RpcClient = Guard.NotNull(nameof(rpc), rpc);
RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);

BannedUtxos = new ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)>();
BannedUtxos = new ConcurrentDictionary<OutPoint, BannedUtxoRecord>();

Directory.CreateDirectory(FolderPath);

Expand All @@ -48,9 +46,9 @@ public UtxoReferee(Network network, string folderPath, RPCClient rpc, CcjRoundCo
string[] allLines = File.ReadAllLines(BannedUtxosFilePath);
foreach (string line in allLines)
{
var bannedRecord = BannedRecordFromLine(line);
var bannedRecord = BannedUtxoRecord.FromString(line);

GetTxOutResponse getTxOutResponse = RpcClient.GetTxOut(bannedRecord.utxo.Hash, (int)bannedRecord.utxo.N, includeMempool: true);
GetTxOutResponse getTxOutResponse = RpcClient.GetTxOut(bannedRecord.Utxo.Hash, (int)bannedRecord.Utxo.N, includeMempool: true);

// Check if inputs are unspent.
if (getTxOutResponse is null)
Expand All @@ -59,7 +57,7 @@ public UtxoReferee(Network network, string folderPath, RPCClient rpc, CcjRoundCo
}
else
{
BannedUtxos.TryAdd(bannedRecord.utxo, (bannedRecord.severity, bannedRecord.timeOfBan, bannedRecord.isNoted, bannedRecord.bannedForRound));
BannedUtxos.TryAdd(bannedRecord.Utxo, bannedRecord);
}
}

Expand Down Expand Up @@ -94,12 +92,12 @@ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, bool for
var updated = false;
foreach (var utxo in toBan)
{
(int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)? foundElem = null;
if (BannedUtxos.TryGetValue(utxo, out (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound) fe))
BannedUtxoRecord foundElem = null;
if (BannedUtxos.TryGetValue(utxo, out BannedUtxoRecord fe))
{
foundElem = fe;
bool bannedForTheSameRound = foundElem.Value.bannedForRound == bannedForRound;
if (bannedForTheSameRound && (!forceNoted || foundElem.Value.isNoted))
bool bannedForTheSameRound = foundElem.BannedForRound == bannedForRound;
if (bannedForTheSameRound && (!forceNoted || foundElem.IsNoted))
{
continue; // We would be simply duplicating this ban.
}
Expand All @@ -114,7 +112,7 @@ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, bool for
{
if (RoundConfig.DosNoteBeforeBan.Value)
{
if (foundElem.HasValue)
if (foundElem != null)
{
isNoted = false;
}
Expand All @@ -124,17 +122,19 @@ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, bool for
isNoted = false;
}
}
if (BannedUtxos.TryAdd(utxo, (severity, timeOfBan, isNoted, bannedForRound)))

var newElem = new BannedUtxoRecord(utxo, severity, timeOfBan, isNoted, bannedForRound);
if (BannedUtxos.TryAdd(newElem.Utxo, newElem))
{
string line = BannedRecordToLine(utxo, severity, timeOfBan, isNoted, bannedForRound);
string line = newElem.ToString();
lines.Add(line);
}
else
{
var elem = BannedUtxos[utxo];
if (elem.isNoted != isNoted || elem.bannedForRound != bannedForRound)
if (elem.IsNoted != isNoted || elem.BannedForRound != bannedForRound)
{
BannedUtxos[utxo] = (elem.severity, elem.timeOfBan, isNoted, bannedForRound);
BannedUtxos[utxo] = new BannedUtxoRecord(elem.Utxo, elem.Severity, elem.TimeOfBan, isNoted, bannedForRound);
updated = true;
}
}
Expand All @@ -144,7 +144,7 @@ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, bool for

if (updated) // If at any time we set updated then we must update the whole thing.
{
var allLines = BannedUtxos.Select(x => $"{x.Value.timeOfBan.ToString(CultureInfo.InvariantCulture)}:{x.Value.severity}:{x.Key.N}:{x.Key.Hash}:{x.Value.isNoted}:{x.Value.bannedForRound}");
var allLines = BannedUtxos.Select(x => $"{x.Value.TimeOfBan.ToString(CultureInfo.InvariantCulture)}:{x.Value.Severity}:{x.Key.N}:{x.Key.Hash}:{x.Value.IsNoted}:{x.Value.BannedForRound}");
await File.WriteAllLinesAsync(BannedUtxosFilePath, allLines);
}
else if (lines.Count != 0) // If we don't have to update the whole thing, we must check if we added a line and so only append.
Expand All @@ -157,26 +157,25 @@ public async Task UnbanAsync(OutPoint output)
{
if (BannedUtxos.TryRemove(output, out _))
{
IEnumerable<string> lines = BannedUtxos.Select(x => BannedRecordToLine(x.Key, x.Value.severity, x.Value.timeOfBan, x.Value.isNoted, x.Value.bannedForRound));
IEnumerable<string> lines = BannedUtxos.Select(x => x.ToString());
await File.WriteAllLinesAsync(BannedUtxosFilePath, lines);
Logger.LogInfo<UtxoReferee>($"UTXO unbanned: {output.N}:{output.Hash}.");
}
}

public async Task<(int severity, TimeSpan bannedRemaining, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)?> TryGetBannedAsync(OutPoint outpoint, bool notedToo)
public async Task<BannedUtxoRecord> TryGetBannedAsync(OutPoint outpoint, bool notedToo)
{
if (BannedUtxos.TryGetValue(outpoint, out (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound) bannedElem))
if (BannedUtxos.TryGetValue(outpoint, out BannedUtxoRecord bannedElem))
{
int maxBan = (int)TimeSpan.FromHours(RoundConfig.DosDurationHours.Value).TotalMinutes;
var bannedRemaining = DateTimeOffset.UtcNow - bannedElem.timeOfBan;
int banLeftMinutes = maxBan - (int)bannedRemaining.TotalMinutes;
int banLeftMinutes = maxBan - (int)bannedElem.BannedRemaining.TotalMinutes;
if (banLeftMinutes > 0)
{
if (bannedElem.isNoted)
if (bannedElem.IsNoted)
{
if (notedToo)
{
return (bannedElem.severity, bannedRemaining, bannedElem.timeOfBan, true, bannedElem.bannedForRound);
return new BannedUtxoRecord(bannedElem.Utxo, bannedElem.Severity, bannedElem.TimeOfBan, true, bannedElem.BannedForRound);
}
else
{
Expand All @@ -185,7 +184,7 @@ public async Task<(int severity, TimeSpan bannedRemaining, DateTimeOffset timeOf
}
else
{
return (bannedElem.severity, bannedRemaining, bannedElem.timeOfBan, false, bannedElem.bannedForRound);
return new BannedUtxoRecord(bannedElem.Utxo, bannedElem.Severity, bannedElem.TimeOfBan, false, bannedElem.BannedForRound);
}
}
else
Expand All @@ -204,7 +203,7 @@ public int CountBanned(bool notedToo)
}
else
{
return BannedUtxos.Count(x => !x.Value.isNoted);
return BannedUtxos.Count(x => !x.Value.IsNoted);
}
}

Expand All @@ -213,23 +212,5 @@ public void Clear()
BannedUtxos.Clear();
File.Delete(BannedUtxosFilePath);
}

internal static string BannedRecordToLine(OutPoint utxo, int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)
{
return $"{timeOfBan.ToString("yyyy-MM-dd HH-mm-ss")}:{severity}:{utxo.N}:{utxo.Hash}:{isNoted}:{bannedForRound}";
}

internal static (OutPoint utxo, int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound) BannedRecordFromLine(string line)
{
var parts = line.Split(':');
var isNoted = bool.Parse(parts[4]);
var bannedForRound = long.Parse(parts[5]);
var utxo = new OutPoint(new uint256(parts[3]), int.Parse(parts[2]));
var severity = int.Parse(parts[1]);
var timeParts = parts[0].Split('-', ' ').Select(x => int.Parse(x)).ToArray();
var timeOfBan = new DateTimeOffset(timeParts[0], timeParts[1], timeParts[2], timeParts[3], timeParts[4], timeParts[5], TimeSpan.Zero);

return (utxo, severity, timeOfBan, isNoted, bannedForRound);
}
}
}

0 comments on commit f547b27

Please sign in to comment.