Add `CoinJoinTransactionArchiver`.
MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -10,7 +10,6 @@ using WalletWasabi.BitcoinCore.Mempool;
using WalletWasabi.BitcoinCore.Rpc;
using WalletWasabi.Blockchain.BlockFilters;
using WalletWasabi.Blockchain.Blocks;
-using WalletWasabi.Blockchain.Mempool;
using WalletWasabi.CoinJoin.Coordinator;
using WalletWasabi.CoinJoin.Coordinator.Rounds;
using WalletWasabi.Helpers;

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/Rounds/Utils/CoinJoinTransactionArchiverTests.cs
+using NBitcoin;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using WalletWasabi.WabiSabi.Backend.Rounds.Utils;
+using Xunit;
+using static WalletWasabi.WabiSabi.Backend.Rounds.Utils.CoinJoinTransactionArchiver;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ /// <summary>
+ /// Tests for <see cref="CoinJoinTransactionArchiver"/>
+ /// </summary>
+ public class CoinJoinTransactionArchiverTests
+ {
+ [Fact]
+ public async Task StoreTransactionAsync()
+ {
+ string tempFolder = Path.GetTempPath();
+ CoinJoinTransactionArchiver archiver = new(tempFolder);
+
+ Transaction randomTx = Network.TestNet.Consensus.ConsensusFactory.CreateTransaction();
+
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+ string storagePath = await archiver.StoreJsonAsync(randomTx, now);
+
+ TransactionInfo transactionInfo = JsonSerializer.Deserialize<TransactionInfo>(File.ReadAllText(storagePath))!;
+ Assert.NotNull(transactionInfo);
+ Assert.Equal(now.ToUnixTimeMilliseconds(), transactionInfo.Created);
+ Assert.Equal(randomTx.GetHash().ToString(), transactionInfo.TxHash);
+ Assert.Equal(randomTx.ToHex(), transactionInfo.RawTransaction);
+ }
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -15,20 +15,21 @@ using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Crypto.CredentialRequesting;
-using WalletWasabi.WabiSabi.Crypto;
using WalletWasabi.WabiSabi.Models;
using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
+using WalletWasabi.WabiSabi.Backend.Rounds.Utils;
namespace WalletWasabi.WabiSabi.Backend.Rounds
{
public class Arena : PeriodicRunner
{
- public Arena(TimeSpan period, Network network, WabiSabiConfig config, IRPCClient rpc, Prison prison) : base(period)
+ public Arena(TimeSpan period, Network network, WabiSabiConfig config, IRPCClient rpc, Prison prison, CoinJoinTransactionArchiver? archiver = null) : base(period)
{
Network = network;
Config = config;
Rpc = rpc;
Prison = prison;
+ TransactionArchiver = archiver;
Random = new SecureRandom();
}
@@ -39,6 +40,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
public IRPCClient Rpc { get; }
public Prison Prison { get; }
public SecureRandom Random { get; }
+ public CoinJoinTransactionArchiver? TransactionArchiver { get; }
public IEnumerable<Round> ActiveRounds => Rounds.Where(x => x.Phase != Phase.Ended);
@@ -188,7 +190,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
{
if (state.IsFullySigned)
{
- var coinjoin = state.CreateTransaction();
+ Transaction coinjoin = state.CreateTransaction();
// Logging.
round.LogInfo("Trying to broadcast coinjoin.");
@@ -209,6 +211,12 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
}
round.LogInfo($"There are {indistinguishableOutputs.Count(x => x.count == 1)} occurrences of unique outputs.");
+ // Store transaction.
+ if (TransactionArchiver is not null)
+ {
+ await TransactionArchiver.StoreJsonAsync(coinjoin).ConfigureAwait(false);
+ }
+
// Broadcasting.
await Rpc.SendRawTransactionAsync(coinjoin, cancellationToken).ConfigureAwait(false);
round.WasTransactionBroadcast = true;

ADD
WalletWasabi/WabiSabi/Backend/Rounds/Utils/CoinJoinTransactionArchiver.cs
+using NBitcoin;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.WabiSabi.Backend.Rounds.Utils
+{
+ public class CoinJoinTransactionArchiver
+ {
+ private static readonly JsonSerializerOptions Options = new()
+ {
+ WriteIndented = true, // Pretty print.
+ };
+
+ public CoinJoinTransactionArchiver(string directoryPath)
+ {
+ DirectoryPath = directoryPath;
+ IoHelpers.EnsureDirectoryExists(directoryPath);
+ }
+
+ /// <summary>Path where to store transactions.</summary>
+ public string DirectoryPath { get; }
+
+ /// <summary>
+ /// Save CoinJoin transaction in a JSON file for later inspection.
+ /// </summary>
+ /// <param name="transaction">Transaction to store.</param>
+ /// <param name="currentDate">Date when the transaction was created or <c>null</c> to use current UTC time.</param>
+ /// <returns>Full path to the stored file.</returns>
+ public async Task<string> StoreJsonAsync(Transaction transaction, DateTimeOffset? currentDate = null)
+ {
+ currentDate ??= DateTimeOffset.UtcNow;
+
+ long unixTimeStampMs = currentDate.Value.ToUnixTimeMilliseconds();
+ string txBytes = transaction.ToHex();
+
+ // Serialize entry.
+ TransactionInfo entry = new(unixTimeStampMs, transaction.GetHash().ToString(), txBytes);
+ string json = JsonSerializer.Serialize(entry, Options);
+
+ // Add UNIX timestamp to the file name to let the files be sorted by date by default.
+ string fileName = $"CoinJoinTransaction.{unixTimeStampMs}.{transaction.GetHash()}.json";
+ string filePath = Path.Combine(DirectoryPath, fileName);
+ await File.WriteAllTextAsync(filePath, json, CancellationToken.None).ConfigureAwait(false);
+
+ return filePath;
+ }
+
+ /// <summary>
+ /// Represents a single CoinJoin transaction.
+ /// </summary>
+ /// <param name="Created">UNIX timestamp in milliseconds when the transaction was created.</param>
+ /// <param name="TxHash">Hash of the transaction.</param>
+ /// <param name="RawTransaction">Raw bitcoin transaction in hexadecimal representation.</param>
+ public record TransactionInfo(
+ [property: JsonPropertyName("createdMs")] long Created,
+ [property: JsonPropertyName("txHash")] string TxHash,
+ [property: JsonPropertyName("txRawHex")] string RawTransaction
+ );
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
@@ -8,8 +8,6 @@ using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Logging;
using WalletWasabi.Models;
-using WalletWasabi.Tor.Socks5.Pool.Circuits;
-using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.Wallets;
using WalletWasabi.WebClients.Wasabi;

MODIFY
WalletWasabi/WabiSabi/CoordinatorParameters.cs
WalletWasabi/WabiSabi/CoordinatorParameters.cs
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using WalletWasabi.BitcoinCore.Rpc;
using WalletWasabi.WabiSabi.Backend;
namespace WalletWasabi.WabiSabi

MODIFY
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
using Microsoft.Extensions.Hosting;
-using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.BitcoinCore.Rpc;
@@ -13,6 +9,7 @@ using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Backend.Rounds.Utils;
namespace WalletWasabi.WabiSabi
{
@@ -26,6 +23,7 @@ namespace WalletWasabi.WabiSabi
Warden = new(parameters.UtxoWardenPeriod, parameters.PrisonFilePath, Config);
ConfigWatcher = new(parameters.ConfigChangeMonitoringPeriod, Config, () => Logger.LogInfo("WabiSabi configuration has changed."));
+ CoinJoinTransactionArchiver transactionArchiver = new(Path.Combine(parameters.CoordinatorDataDir, "CoinJoinTransactions"));
Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Prison);
Postman = new(Config, Prison, Arena, Rpc);

Add `CoinJoinTransactionArchiver`.