Finish coordinator implementation
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -50,7 +50,7 @@ namespace WalletWasabi.Backend.Controllers
{
Phase = round.Phase,
Denomination = round.Denomination,
- RegisteredPeerCount = round.CountAlices(syncronized: false),
+ RegisteredPeerCount = round.CountAlices(syncLock: false),
RequiredPeerCount = round.AnonymitySet,
MaximumInputCountPerPeer = 7, // Constant for now. If we want to do something with it later, we'll put it to the config file.
RegistrationTimeout = (int)round.AliceRegistrationTimeout.TotalSeconds,
@@ -536,7 +536,7 @@ namespace WalletWasabi.Backend.Controllers
return NotFound("Round not found.");
}
- if (null == round.TryGetAliceBy(uniqueIdGuid)) // We don't care about Alice now, but let's not give the CJ to anyone who isn't participating.
+ if (round.TryGetAliceBy(uniqueIdGuid) == null) // We don't care about Alice now, but let's not give the CJ to anyone who isn't participating.
{
return NotFound("Alice not found.");
}
@@ -560,16 +560,31 @@ namespace WalletWasabi.Backend.Controllers
}
}
+ private static AsyncLock SigningLock { get; } = new AsyncLock();
+
/// <summary>
- /// Alice posts her partial signatures.
+ /// Alice posts her witnesses.
/// </summary>
- /// <response code="400">The provided uniqueId or roundId was malformed.</response>
+ /// <param name="uniqueId">Unique identifier, obtained previously.</param>
+ /// <param name="roundId">Round identifier, obtained previously.</param>
+ /// <param name="signatures">Dictionary that has an int index as its key and string witness as its value.</param>
+ /// <returns>Hx of the coinjoin transaction.</returns>
+ /// <response code="204">CoinJoin successfully signed.</response>
+ /// <response code="400">The provided uniqueId, roundId or witnesses were malformed.</response>
+ /// <response code="403">CoinJoin can only be requested from a Running round's Signing phase.</response>
+ /// <response code="404">If Alice or the round is not found.</response>
[HttpPost("signatures")]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
- public IActionResult PostSignatures([FromQuery]string uniqueId, [FromQuery]long roundId)
+ [ProducesResponseType(403)]
+ [ProducesResponseType(404)]
+ public async Task<IActionResult> PostSignaturesAsync([FromQuery]string uniqueId, [FromQuery]long roundId, [FromBody]IDictionary<int, string> signatures)
{
- if (roundId <= 0 || !ModelState.IsValid)
+ if (roundId <= 0
+ || signatures == null
+ || signatures.Count() <= 0
+ || signatures.Any(x => x.Key < 0 || string.IsNullOrWhiteSpace(x.Value))
+ || !ModelState.IsValid)
{
return BadRequest();
}
@@ -580,8 +595,86 @@ namespace WalletWasabi.Backend.Controllers
return returnFailureResponse;
}
+ CcjRound round = Coordinator.TryGetRound(roundId);
+ if (round == null)
+ {
+ return NotFound("Round not found.");
+ }
+
+ Alice alice = round.TryGetAliceBy(uniqueIdGuid);
+ if (alice == null)
+ {
+ return NotFound("Alice not found.");
+ }
+
+ // Check if Alice provided signature to all her inputs.
+ if (signatures.Count != alice.Inputs.Count())
+ {
+ return BadRequest("Alice did not provide enough witnesses.");
+ }
+
+ if (round.Status != CcjRoundStatus.Running)
+ {
+ return Forbid("Round is not running.");
+ }
+
+ CcjRoundPhase phase = round.Phase;
+ switch (phase)
+ {
+ case CcjRoundPhase.InputRegistration:
+ {
+ using (await SigningLock.LockAsync())
+ {
+ foreach (var signaturePair in signatures)
+ {
+ int index = signaturePair.Key;
+ WitScript witness = null;
+ try
+ {
+ witness = new WitScript(signaturePair.Value);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Malformed witness is provided. Details: {ex.Message}");
+ }
+ int maxIndex = round.UnsignedCoinJoin.Inputs.Count - 1;
+ if (maxIndex < index)
+ {
+ return BadRequest($"Index out of range. Maximum value: {maxIndex}. Provided value: {index}");
+ }
+
+ // Check duplicates.
+ if(string.IsNullOrWhiteSpace(round.SignedCoinJoin.Inputs[index].WitScript?.ToString())) // Not sure why WitScript?.ToString() is needed, there was something wrong in previous HiddenWallet version if I didn't do this.
+ {
+ return BadRequest($"Input is already signed.");
+ }
+
+ // Verify witness.
+ var cjCopy = new Transaction(round.UnsignedCoinJoin.ToHex());
+ cjCopy.Inputs[index].WitScript = witness;
+ TxOut output = alice.Inputs.Single(x => x.OutPoint == cjCopy.Inputs[index].PrevOut).Output;
+ if (!Script.VerifyScript(output.ScriptPubKey, cjCopy, index, output.Value, ScriptVerify.Standard, SigHash.All))
+ {
+ return BadRequest($"Invalid witness is provided.");
+ }
+
+ // Finally add it to our CJ.
+ round.SignedCoinJoin.Inputs[index].WitScript = witness;
+ }
+
+ alice.State = AliceState.SignedCoinJoin;
+
+ await round.BroadcastCoinJoinIfFullySignedAsync();
+ }
+
return NoContent();
}
+ default:
+ {
+ return Forbid($"CoinJoin can only be requested from Signing phase. Current phase: {phase}.");
+ }
+ }
+ }
private Guid CheckUniqueId(string uniqueId, out IActionResult returnFailureResponse)
{

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
@@ -76,7 +76,6 @@ namespace WalletWasabi.ChaumianCoinJoin
private void OnPhaseChanged(CcjRoundPhase phase) => PhaseChanged?.Invoke(this, phase);
private CcjRoundStatus _status;
- private int _myProperty;
private object StatusLock { get; }
public CcjRoundStatus Status
@@ -299,6 +298,8 @@ namespace WalletWasabi.ChaumianCoinJoin
.Shuffle()
.BuildTransaction(false);
+ SignedCoinJoin = new Transaction(UnsignedCoinJoin.ToHex());
+
Phase = CcjRoundPhase.Signing;
}
else
@@ -430,18 +431,41 @@ namespace WalletWasabi.ChaumianCoinJoin
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
- public void Fail()
+ public void Succeed(bool syncLock = true)
+ {
+ if (syncLock)
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ Status = CcjRoundStatus.Succeded;
+ }
+ }
+ else
+ {
+ Status = CcjRoundStatus.Succeded;
+ }
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Succeeded.");
+ }
+
+ public void Fail(bool syncLock = true)
+ {
+ if (syncLock)
{
using (RoundSyncronizerLock.Lock())
{
Status = CcjRoundStatus.Failed;
}
+ }
+ else
+ {
+ Status = CcjRoundStatus.Failed;
+ }
Logger.LogInfo<CcjRound>($"Round ({RoundId}): Failed.");
}
- public int CountAlices(bool syncronized = true)
+ public int CountAlices(bool syncLock = true)
{
- if (syncronized)
+ if (syncLock)
{
using (RoundSyncronizerLock.Lock())
{
@@ -594,6 +618,38 @@ namespace WalletWasabi.ChaumianCoinJoin
#region Modifiers
+ public async Task BroadcastCoinJoinIfFullySignedAsync()
+ {
+ using (await RoundSyncronizerLock.LockAsync())
+ {
+ // Check if fully signed.
+ if (SignedCoinJoin.Inputs.All(x => !string.IsNullOrWhiteSpace(x.WitScript?.ToString()))) // Not sure why WitScript?.ToString() is needed, there was something wrong in previous HiddenWallet version if I didn't do this.
+ {
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Trying to broadcast coinjoin.");
+
+ try
+ {
+ Coin[] spentCoins = Alices.SelectMany(x => x.Inputs.Select(y => new Coin(y.OutPoint, y.Output))).ToArray();
+ Money networkFee = SignedCoinJoin.GetFee(spentCoins);
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Network Fee: {networkFee.ToString(false, false)} BTC.");
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Coordinator Fee: {SignedCoinJoin.Outputs.Single(x=>x.ScriptPubKey == Constants.GetCoordinatorAddress(RpcClient.Network).ScriptPubKey).Value.ToString(false, false)} BTC.");
+ FeeRate feeRate = SignedCoinJoin.GetFeeRate(spentCoins);
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Network Fee Rate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} satoshi/byte.");
+
+ await RpcClient.SendRawTransactionAsync(SignedCoinJoin);
+ Succeed(syncLock: false);
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Successfully broadcasted the CoinJoin: {SignedCoinJoin.GetHash()}.");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError<CcjRound>($"Round ({RoundId}): Failed to broadcast the CoinJoin: {SignedCoinJoin.GetHash()}.");
+ Logger.LogError<CcjRound>(ex);
+ Fail(syncLock: false);
+ }
+ }
+ }
+ }
+
public void UpdateAnonymitySet(int anonymitySet)
{
using (RoundSyncronizerLock.Lock())

Finish coordinator implementation