Finetune edge cases
MODIFY
HiddenWallet.ChaumianTumbler/Configuration/Config.cs
HiddenWallet.ChaumianTumbler/Configuration/Config.cs
@@ -64,6 +64,12 @@ namespace HiddenWallet.ChaumianTumbler.Configuration
[JsonProperty(PropertyName = "FallBackSatoshiFeePerBytes")]
public int? FallBackSatoshiFeePerBytes { get; private set; }
+ [JsonProperty(PropertyName = "FeeConfirmationTarget")]
+ public int? FeeConfirmationTarget { get; private set; }
+
+ [JsonProperty(PropertyName = "FeeEstimationMode")]
+ public string FeeEstimationMode { get; private set; }
+
public Config()
{
@@ -99,6 +105,8 @@ namespace HiddenWallet.ChaumianTumbler.Configuration
SigningPhaseTimeoutInSeconds = 60;
MaximumInputsPerAlices = 7;
FallBackSatoshiFeePerBytes = 300;
+ FeeConfirmationTarget = 2;
+ FeeEstimationMode = "ECONOMICAL";
if (!File.Exists(path))
{
@@ -124,6 +132,8 @@ namespace HiddenWallet.ChaumianTumbler.Configuration
SigningPhaseTimeoutInSeconds = config.SigningPhaseTimeoutInSeconds ?? SigningPhaseTimeoutInSeconds;
MaximumInputsPerAlices = config.MaximumInputsPerAlices ?? MaximumInputsPerAlices;
FallBackSatoshiFeePerBytes = config.FallBackSatoshiFeePerBytes ?? FallBackSatoshiFeePerBytes;
+ FeeConfirmationTarget = config.FeeConfirmationTarget ?? FeeConfirmationTarget;
+ FeeEstimationMode = config.FeeEstimationMode ?? FeeEstimationMode;
}
await ToFileAsync(path, cancel);

MODIFY
HiddenWallet.ChaumianTumbler/Controllers/TumblerController.cs
HiddenWallet.ChaumianTumbler/Controllers/TumblerController.cs
@@ -378,13 +378,15 @@ namespace HiddenWallet.ChaumianTumbler.Controllers
await Global.UtxoReferee.BanAliceAsync(alice);
Global.StateMachine.FallBackRound = true;
Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
+ throw new ArgumentOutOfRangeException(nameof(signatureModel.Index));
}
- if(coinJoin.Inputs[signatureModel.Index].WitScript != default)
+ if(!string.IsNullOrWhiteSpace(coinJoin.Inputs[signatureModel.Index]?.WitScript?.ToString()))
{
// round fails, ban alice
await Global.UtxoReferee.BanAliceAsync(alice);
Global.StateMachine.FallBackRound = true;
Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
+ throw new InvalidOperationException("Input is already signed");
}
coinJoin.Inputs[signatureModel.Index].WitScript = witness;
var output = alice.Inputs.Single(x => x.OutPoint == coinJoin.Inputs[signatureModel.Index].PrevOut).Output;
@@ -394,6 +396,7 @@ namespace HiddenWallet.ChaumianTumbler.Controllers
await Global.UtxoReferee.BanAliceAsync(alice);
Global.StateMachine.FallBackRound = true;
Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
+ throw new InvalidOperationException("Invalid witness");
}
else
{
@@ -404,32 +407,40 @@ namespace HiddenWallet.ChaumianTumbler.Controllers
// check if fully signed
if(Global.StateMachine.FullySignedCoinJoin)
{
- var failed = true;
+ Console.WriteLine("Trying to propagate coinjoin....");
+ ConcurrentHashSet<Alice> alices = Global.StateMachine.Alices;
+ Coin[] spentCoins = alices.SelectMany(x => x.Inputs.Select(y => new Coin(y.OutPoint, y.Output))).ToArray();
+ var fee = coinJoin.GetFee(spentCoins);
+ Console.WriteLine($"Fee: {fee.ToString(false, true)}");
+ var feeRate = coinJoin.GetFeeRate(spentCoins);
+ Console.WriteLine($"FeeRate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} satoshi/byte");
+
+ var state = CoinJoinTransactionState.Failed;
try
{
var res = await Global.RpcClient.SendCommandAsync(RPCOperations.sendrawtransaction, coinJoin.ToHex(), true);
- var state = CoinJoinTransactionState.Failed;
if (coinJoin.GetHash().ToString() == res.ResultString)
{
- failed = false;
state = CoinJoinTransactionState.Succeeded;
Console.WriteLine($"Propagated transaction: {coinJoin.GetHash()}");
}
- Global.CoinJoinStore.Transactions.Add(new CoinJoinTransaction
- {
- Transaction = coinJoin,
- DateTime = DateTimeOffset.UtcNow,
- State = state
- });
- await Global.CoinJoinStore.ToFileAsync(Global.CoinJoinStorePath);
}
finally
{
- if (failed)
+ if (state == CoinJoinTransactionState.Failed)
{
Global.StateMachine.FallBackRound = true;
}
else Global.StateMachine.FallBackRound = false;
+
+ Global.CoinJoinStore.Transactions.Add(new CoinJoinTransaction
+ {
+ Transaction = coinJoin,
+ DateTime = DateTimeOffset.UtcNow,
+ State = state
+ });
+ await Global.CoinJoinStore.ToFileAsync(Global.CoinJoinStorePath);
+
Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
}
}

MODIFY
HiddenWallet.ChaumianTumbler/Global.cs
HiddenWallet.ChaumianTumbler/Global.cs
@@ -101,9 +101,9 @@ namespace HiddenWallet.ChaumianTumbler
throw new NotSupportedException("blocks != headers");
}
- if (string.IsNullOrWhiteSpace((await RpcClient.SendCommandAsync("estimatesmartfee", 1, "ECONOMICAL"))?.ResultString))
+ if (string.IsNullOrWhiteSpace((await RpcClient.SendCommandAsync("estimatesmartfee", Config.FeeConfirmationTarget, Config.FeeEstimationMode))?.ResultString))
{
- throw new NotSupportedException("estimatesmartfee 1 ECONOMICAL == null");
+ throw new NotSupportedException($"estimatesmartfee {Config.FeeConfirmationTarget} {Config.FeeEstimationMode} == null");
}
}
catch

MODIFY
HiddenWallet.ChaumianTumbler/TumblerStateMachine.cs
HiddenWallet.ChaumianTumbler/TumblerStateMachine.cs
@@ -250,17 +250,19 @@ namespace HiddenWallet.ChaumianTumbler
{
int inputSizeInBytes = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m); // vSize
int outputSizeInBytes = Constants.OutputSizeInBytes;
+
try
{
- RPCResponse result = (await Global.RpcClient.SendCommandAsync("estimatesmartfee", 1, "ECONOMICAL"));
+ RPCResponse result = (await Global.RpcClient.SendCommandAsync("estimatesmartfee", Global.Config.FeeConfirmationTarget, Global.Config.FeeEstimationMode));
if (string.IsNullOrWhiteSpace(result?.ResultString)) throw new ArgumentException(nameof(result));
var resultJToken = result.Result;
var feeRateDecimal = resultJToken.Value<decimal>("feerate");
var feeRate = new FeeRate(new Money(feeRateDecimal, MoneyUnit.BTC));
Money feePerBytes = (feeRate.FeePerK / 1000);
- FeePerInputs = feePerBytes * inputSizeInBytes;
- FeePerOutputs = feePerBytes * outputSizeInBytes;
+ // make sure min relay fee (1000 sat) is hit
+ FeePerInputs = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
+ FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
}
catch
{
@@ -269,8 +271,9 @@ namespace HiddenWallet.ChaumianTumbler
{
var feePerBytes = new Money((int)Global.Config.FallBackSatoshiFeePerBytes);
- FeePerInputs = feePerBytes * inputSizeInBytes;
- FeePerOutputs = feePerBytes * outputSizeInBytes;
+ // make sure min relay fee (1000 sat) is hit
+ FeePerInputs = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
+ FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
}
}
}

MODIFY
HiddenWallet.Daemon/Config.cs
HiddenWallet.Daemon/Config.cs
@@ -80,6 +80,8 @@ namespace HiddenWallet.Daemon
CanSpendUnconfirmed = config.CanSpendUnconfirmed ?? CanSpendUnconfirmed;
ChaumianTumblerTestNetAddress = config.ChaumianTumblerTestNetAddress ?? ChaumianTumblerTestNetAddress;
ChaumianTumblerMainAddress = config.ChaumianTumblerMainAddress ?? ChaumianTumblerMainAddress;
+ ChaumianTumblerTestNetNotificationAddress = config.ChaumianTumblerTestNetNotificationAddress ?? ChaumianTumblerTestNetNotificationAddress;
+ ChaumianTumblerMainNotificationAddress = config.ChaumianTumblerMainNotificationAddress ?? ChaumianTumblerMainNotificationAddress;
}
await ToFileAsync(path, cancel);

MODIFY
HiddenWallet.FullSpvWallet/ChaumianCoinJoin/CoinJoinService.cs
HiddenWallet.FullSpvWallet/ChaumianCoinJoin/CoinJoinService.cs
@@ -88,7 +88,7 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
{
try
{
- await ExecutePhaseAsync(Phase, CancellationToken.None);
+ await ExecutePhaseAsync(Phase);
}
catch
{
@@ -104,14 +104,17 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
}
catch (Exception ex)
{
- await DisposeAsync();
+ await TumblerConnection?.DisposeAsync();
+ TumblerConnection = null;
+
Debug.WriteLine(ex);
}
}
private async Task TumblerConnection_ClosedAsync(Exception arg)
{
- await DisposeAsync();
+ await TumblerConnection?.DisposeAsync();
+ TumblerConnection = null;
}
#endregion
@@ -161,11 +164,12 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
// wait for input registration
while(Phase != TumblerPhase.InputRegistration)
{
+ status = null;
await Task.Delay(100, cancel);
}
TumblingInProcess = true;
- await ExecutePhaseAsync(Phase, cancel);
+ await ExecutePhaseAsync(Phase, status, cancel);
// wait while tumbling is in process
while (TumblingInProcess)
@@ -189,7 +193,7 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
}
}
- private async Task ExecutePhaseAsync(TumblerPhase phase, CancellationToken cancel)
+ private async Task ExecutePhaseAsync(TumblerPhase phase, StatusResponse statusJustAcquired = null, CancellationToken cancel = default)
{
while (CompletedLastPhase == false)
{
@@ -202,7 +206,7 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
CompletedLastPhase = false;
if (phase == TumblerPhase.InputRegistration)
{
- StatusResponse status = await TumblerClient.GetStatusAsync(cancel);
+ StatusResponse status = statusJustAcquired ?? await TumblerClient.GetStatusAsync(cancel);
// TODO: only native segwit (p2wpkh) inputs are accepted by the tumbler (later wrapped segwit (p2sh-p2wpkh) will be accepted too)
var getBalanceResult = await WalletJob.GetBalanceAsync(From);
var balance = getBalanceResult.Available.Confirmed + getBalanceResult.Available.Unconfirmed;

Finetune edge cases