Skip to content
Permalink
Browse files

Enable to remix mixed outputs

Resolves #833
  • Loading branch information...
nopara73 committed Nov 25, 2018
1 parent 3a50e57 commit 81f5a9ff1bcdeaeaff876c57f3c9200ab53ad962
@@ -1697,19 +1697,19 @@ public async Task CcjTestsAsync()
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await AliceClient.CreateNewAsync(network, inputsRequest, baseUri));
Assert.StartsWith($"{HttpStatusCode.BadRequest.ToReasonString()}\nNot enough inputs are provided. Fee to pay:", httpRequestException.Message);

roundConfig.Denomination = Money.Coins(0.01m); // exactly the same as our output
roundConfig.SetDenomination(Money.Coins(0.01m)); // exactly the same as our output
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await AliceClient.CreateNewAsync(network, inputsRequest, baseUri));
Assert.StartsWith($"{HttpStatusCode.BadRequest.ToReasonString()}\nNot enough inputs are provided. Fee to pay:", httpRequestException.Message);

roundConfig.Denomination = Money.Coins(0.00999999m); // one satoshi less than our output
roundConfig.SetDenomination(Money.Coins(0.00999999m)); // one satoshi less than our output
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await AliceClient.CreateNewAsync(network, inputsRequest, baseUri));
Assert.StartsWith($"{HttpStatusCode.BadRequest.ToReasonString()}\nNot enough inputs are provided. Fee to pay:", httpRequestException.Message);

roundConfig.Denomination = Money.Coins(0.008m); // one satoshi less than our output
roundConfig.SetDenomination(Money.Coins(0.008m)); // one satoshi less than our output
roundConfig.ConnectionConfirmationTimeout = 2;
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
@@ -2545,7 +2545,7 @@ public async Task Ccj100ParticipantsTestsAsync()
Assert.True(2 * feeRateReal.FeePerK > feeRateTx.FeePerK); // Max 200% mistake.

var activeOutput = finalCoinjoin.GetIndistinguishableOutputs().OrderByDescending(x => x.count).First();
Assert.True(activeOutput.value >= roundConfig.Denomination);
Assert.True(activeOutput.value >= roundConfig.CurrentDenomination);
Assert.True(activeOutput.value >= roundConfig.AnonymitySet);

foreach (var aliceClient in aliceClients)
@@ -131,7 +131,7 @@ public CcjRound(RPCClient rpc, UtxoReferee utxoReferee, CcjRoundConfig config)
UtxoReferee = Guard.NotNull(nameof(utxoReferee), utxoReferee);
Guard.NotNull(nameof(config), config);

Denomination = config.Denomination;
Denomination = config.CurrentDenomination;
ConfirmationTarget = (int)config.ConfirmationTarget;
CoordinatorFeePercent = (decimal)config.CoordinatorFeePercent;
AnonymitySet = (int)config.AnonymitySet;
@@ -168,7 +168,7 @@ public CcjRound(RPCClient rpc, UtxoReferee utxoReferee, CcjRoundConfig config)
}
}

public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase)
public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase, Money feePerInputs = null, Money feePerOutputs = null)
{
using (await RoundSynchronizerLock.LockAsync())
{
@@ -183,33 +183,17 @@ public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase)
return;
}

// Calculate fees
var inputSizeInBytes = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m);
var outputSizeInBytes = Constants.OutputSizeInBytes;
try
// Calculate fees.
if (feePerInputs is null || feePerOutputs is null)
{
var estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, tryOtherFeeRates: true);
if (estimateSmartFeeResponse is null) throw new InvalidOperationException("FeeRate is not yet initialized");
var feeRate = estimateSmartFeeResponse.FeeRate;
Money feePerBytes = (feeRate.FeePerK / 1000);

// 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));
(Money feePerInputs, Money feePerOutputs) fees = await CalculateFeesAsync(RpcClient, ConfirmationTarget);
FeePerInputs = feePerInputs ?? fees.feePerInputs;
FeePerOutputs = feePerOutputs ?? fees.feePerOutputs;
}
catch (Exception ex)
else
{
// If fee hasn't been initialized once, fall back.
if (FeePerInputs is null || FeePerOutputs is null)
{
var feePerBytes = new Money(100); // 100 satoshi per byte

// 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));
}

Logger.LogError<CcjRound>(ex);
FeePerInputs = feePerInputs;
FeePerOutputs = feePerOutputs;
}

Status = CcjRoundStatus.Running;
@@ -525,6 +509,44 @@ public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase)
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}

public static async Task<(Money feePerInputs, Money feePerOutputs)> CalculateFeesAsync(RPCClient rpc, int confirmationTarget)
{
Guard.NotNull(nameof(rpc), rpc);
Guard.NotNull(nameof(confirmationTarget), confirmationTarget);

Money feePerInputs = null;
Money feePerOutputs = null;
var inputSizeInBytes = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m);
var outputSizeInBytes = Constants.OutputSizeInBytes;
try
{
var estimateSmartFeeResponse = await rpc.EstimateSmartFeeAsync(confirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, tryOtherFeeRates: true);
if (estimateSmartFeeResponse is null) throw new InvalidOperationException("FeeRate is not yet initialized");
var feeRate = estimateSmartFeeResponse.FeeRate;
Money feePerBytes = (feeRate.FeePerK / 1000);

// 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 (Exception ex)
{
// If fee hasn't been initialized once, fall back.
if (feePerInputs is null || feePerOutputs is null)
{
var feePerBytes = new Money(100); // 100 satoshi per byte

// 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));
}

Logger.LogError<CcjRound>(ex);
}

return (feePerInputs, feePerOutputs);
}

public void Succeed(bool syncLock = true)
{
if (syncLock)
@@ -14,12 +14,32 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
[JsonObject(MemberSerialization.OptIn)]
public class CcjRoundConfig : IConfig
{
private Money _denomination;

/// <inheritdoc />
public string FilePath { get; internal set; }

public Money CurrentDenomination { get; internal set; }

[JsonProperty(PropertyName = "Denomination")]
[JsonConverter(typeof(MoneyBtcJsonConverter))]
public Money Denomination { get; internal set; }
private Money Denomination
{
get => _denomination;
set
{
if (value != _denomination)
{
_denomination = value;
CurrentDenomination = value;
}
}
}

internal void SetDenomination(Money denomination)
{
Denomination = denomination;
}

[JsonProperty(PropertyName = "ConfirmationTarget")]
public int? ConfirmationTarget { get; internal set; }
@@ -177,7 +177,7 @@ public void UpdateRoundConfig(CcjRoundConfig roundConfig)
RoundConfig.UpdateOrDefault(roundConfig);
}

public async Task MakeSureTwoRunningRoundsAsync()
public async Task MakeSureTwoRunningRoundsAsync(Money feePerInputs = null, Money feePerOutputs = null)
{
using (await RoundsListLock.LockAsync())
{
@@ -187,35 +187,38 @@ public async Task MakeSureTwoRunningRoundsAsync()
var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
round.StatusChanged += Round_StatusChangedAsync;
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);
Rounds.Add(round);

var round2 = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round2.StatusChanged += Round_StatusChangedAsync;
round2.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
await round2.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
await round2.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);
Rounds.Add(round2);
}
else if (runningRoundCount == 1)
{
var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round.StatusChanged += Round_StatusChangedAsync;
round.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);
Rounds.Add(round);
}
}
}

private void Round_CoinJoinBroadcasted(object sender, Transaction e)
private void Round_CoinJoinBroadcasted(object sender, Transaction transaction)
{
CoinJoinBroadcasted?.Invoke(sender, e);
CoinJoinBroadcasted?.Invoke(sender, transaction);
}

private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status)
{
var round = sender as CcjRound;

Money feePerInputs = null;
Money feePerOutputs = null;

// If success save the coinjoin.
if (status == CcjRoundStatus.Succeded)
{
@@ -224,6 +227,27 @@ private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status
uint256 coinJoinHash = round.SignedCoinJoin.GetHash();
CoinJoins.Add(coinJoinHash);
await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() });

// When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount.
IEnumerable<(Money value, int count)> outputs = round.SignedCoinJoin.GetIndistinguishableOutputs();
var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault();
if (bestOutput != default)
{
Money activeOutputAmount = bestOutput.value;

var fees = await CcjRound.CalculateFeesAsync(RpcClient, RoundConfig.ConfirmationTarget.Value);
feePerInputs = fees.feePerInputs;
feePerOutputs = fees.feePerOutputs;

Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + 2 * feePerOutputs);
if (newDenominationToGetInWithactiveOutputs < RoundConfig.CurrentDenomination)
{
if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
{
RoundConfig.CurrentDenomination = newDenominationToGetInWithactiveOutputs;
}
}
}
}
}

@@ -243,7 +267,7 @@ private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status
{
round.StatusChanged -= Round_StatusChangedAsync;
round.CoinJoinBroadcasted -= Round_CoinJoinBroadcasted;
await MakeSureTwoRunningRoundsAsync();
await MakeSureTwoRunningRoundsAsync(feePerInputs, feePerOutputs);
}
}

0 comments on commit 81f5a9f

Please sign in to comment.
You can’t perform that action at this time.