Add LockTimeSelector + unit test.
MODIFY
WalletWasabi.Tests/UnitTests/Transactions/TransactionFactoryTests.cs
WalletWasabi.Tests/UnitTests/Transactions/TransactionFactoryTests.cs
@@ -561,10 +561,11 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
dict[lockTimeZero] = 0;
var curTip = 100_000u;
- var rnd = new Random(123456);
+ var lockTimeSelector = new LockTimeSelector(new Random(123456));
+
foreach (var i in Enumerable.Range(0, samplingSize))
{
- var lt = (uint)Wallet.InternalSelectLockTimeForTransaction(curTip, rnd).Height;
+ var lt = (uint)lockTimeSelector.GetLockTimeBasedOnDistribution(curTip).Height;
var diff = lt == 0 ? lockTimeZero : lt - curTip;
dict[diff]++;
}

ADD
WalletWasabi.Tests/UnitTests/Wallet/LockTimeSelectorTests.cs
+using NBitcoin;
+using System;
+using WalletWasabi.Wallets;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Wallet
+{
+ public class LockTimeSelectorTests
+ {
+ [Fact]
+ public void GetLockTimeBasedOnDistributionTest()
+ {
+ var lockTimeSelector = new LockTimeSelector(new Random());
+
+ uint tipHeight = 600_000;
+ LockTime lockTime = lockTimeSelector.GetLockTimeBasedOnDistribution(tipHeight);
+
+ if (lockTime.Value == 0)
+ {
+ Assert.Equal(LockTime.Zero, lockTime);
+ }
+ else
+ {
+ Assert.InRange(lockTime.Value, tipHeight - 99, tipHeight + 1);
+ }
+ }
+ }
+}

ADD
WalletWasabi/Wallets/LockTimeSelector.cs
+using NBitcoin;
+using System;
+
+namespace WalletWasabi.Wallets
+{
+ /// <summary>
+ /// Produces random <see cref="LockTime"/> value for a new transaction based on observed lock-time values distribution in Bitcoin mainnet network.
+ /// </summary>
+ /// <remarks>Helps avoid fingerprinting of Wasabi Wallet transactions.</remarks>
+ public class LockTimeSelector
+ {
+ static LockTimeSelector()
+ {
+ Instance = new LockTimeSelector(new Random());
+ }
+
+ public LockTimeSelector(Random random)
+ {
+ Random = random;
+ }
+
+ public static LockTimeSelector Instance { get; }
+
+ private Random Random { get; }
+
+ public LockTime GetLockTimeBasedOnDistribution(uint tipHeight)
+ {
+ // We use the TimeLock distribution observed in the bitcoin network
+ // in order to reduce the wasabi wallet transactions fingerprinting
+ // chances.
+ //
+ // Network observations:
+ // 90.0% uses LockTime = 0
+ // 7.5% uses LockTime = current tip
+ // 0.65% uses LockTime = next tip (current tip + 1)
+ // 1.85% uses up to 5 blocks in the future (we don't do this)
+ // 0.65% uses an uniform random from -1 to -99
+
+ // sometimes pick LockTime a bit further back, to help privacy.
+ var randomValue = Random.NextDouble();
+ return randomValue switch
+ {
+ var r when r < (0.9) => LockTime.Zero,
+ var r when r < (0.9 + 0.075) => tipHeight,
+ var r when r < (0.9 + 0.075 + 0.0065) => tipHeight + 1,
+ _ => (uint)(tipHeight - Random.Next(1, 100))
+ };
+ }
+ }
+}

MODIFY
WalletWasabi/Wallets/Wallet.cs
WalletWasabi/Wallets/Wallet.cs
@@ -2,20 +2,13 @@ using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Protocol;
-using NBitcoin.Protocol.Behaviors;
-using NBitcoin.RPC;
using Nito.AsyncEx;
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Backend.Models;
-using WalletWasabi.BitcoinCore;
using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.Analysis.FeesEstimation;
using WalletWasabi.Blockchain.Keys;
@@ -25,7 +18,6 @@ using WalletWasabi.Blockchain.TransactionProcessing;
using WalletWasabi.Blockchain.Transactions;
using WalletWasabi.CoinJoin.Client.Clients;
using WalletWasabi.CoinJoin.Client.Clients.Queuing;
-using WalletWasabi.Exceptions;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
using WalletWasabi.Models;
@@ -103,7 +95,6 @@ namespace WalletWasabi.Wallets
public IFeeProvider FeeProvider { get; private set; }
public FilterModel LastProcessedFilter { get; private set; }
public IBlockProvider BlockProvider { get; private set; }
- private static Random Random { get; } = new Random();
private AsyncLock HandleFiltersLock { get; }
public void RegisterServices(
@@ -224,7 +215,7 @@ namespace WalletWasabi.Wallets
var builder = new TransactionFactory(Network, KeyManager, Coins, BitcoinStore, password, allowUnconfirmed);
return builder.BuildTransaction(
payments,
- () =>
+ feeRateFetcher: () =>
{
if (feeStrategy.Type == FeeStrategyType.Target)
{
@@ -240,7 +231,11 @@ namespace WalletWasabi.Wallets
}
},
allowedInputs,
- SelectLockTimeForTransaction,
+ lockTimeSelector: () =>
+ {
+ var currentTipHeight = Synchronizer.BitcoinStore.SmartHeaderChain.TipHeight;
+ return LockTimeSelector.Instance.GetLockTimeBasedOnDistribution(currentTipHeight);
+ },
payjoinClient);
}
@@ -282,37 +277,6 @@ namespace WalletWasabi.Wallets
}
}
- internal static LockTime InternalSelectLockTimeForTransaction(uint tipHeight, Random rnd)
- {
- try
- {
- // We use the timelock distribution observed in the bitcoin network
- // in order to reduce the wasabi wallet transactions fingerprinting
- // chances.
- //
- // Network observations:
- // 90.0% uses locktime = 0
- // 7.5% uses locktime = current tip
- // 0.65% uses locktime = next tip (current tip + 1)
- // 1.85% uses up to 5 blocks in the future (we don't do this)
- // 0.65% uses an uniform random from -1 to -99
-
- // sometimes pick locktime a bit further back, to help privacy.
- var randomValue = rnd.NextDouble();
- return randomValue switch
- {
- var r when r < (0.9) => LockTime.Zero,
- var r when r < (0.9 + 0.075) => tipHeight,
- var r when r < (0.9 + 0.075 + 0.0065) => tipHeight + 1,
- _ => (uint)(tipHeight - rnd.Next(1, 100))
- };
- }
- catch
- {
- return LockTime.Zero;
- }
- }
-
private async void TransactionProcessor_WalletRelevantTransactionProcessedAsync(object sender, ProcessedResult e)
{
try
@@ -526,13 +490,6 @@ namespace WalletWasabi.Wallets
LastProcessedFilter = filterModel;
}
- private LockTime SelectLockTimeForTransaction()
- {
- var currentTipHeight = Synchronizer.BitcoinStore.SmartHeaderChain.TipHeight;
-
- return InternalSelectLockTimeForTransaction(currentTipHeight, Random);
- }
-
public void SetWaitingForInitState()
{
if (State != WalletState.Uninitialized)

Add LockTimeSelector + unit test.