Implement POST output
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -84,7 +84,7 @@ namespace WalletWasabi.Backend.Controllers
// Validate request.
if (!ModelState.IsValid
|| request == null
- || string.IsNullOrWhiteSpace(request.BlindedOutputHashHex)
+ || string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex)
|| string.IsNullOrWhiteSpace(request.ChangeOutputScript)
|| request.Inputs == null
|| request.Inputs.Count() == 0
@@ -107,7 +107,7 @@ namespace WalletWasabi.Backend.Controllers
// Do more checks.
try
{
- if (round.ContainsBlindedOutput(request.BlindedOutputHashHex, out _))
+ if (round.ContainsBlindedOutputScriptHex(request.BlindedOutputScriptHex, out _))
{
return BadRequest("Blinded output has already been registered.");
}
@@ -196,7 +196,7 @@ namespace WalletWasabi.Backend.Controllers
bool validProof;
try
{
- validProof = address.VerifyMessage(request.BlindedOutputHashHex, inputProof.Proof);
+ validProof = address.VerifyMessage(request.BlindedOutputScriptHex, inputProof.Proof);
}
catch (FormatException ex)
{
@@ -220,7 +220,7 @@ namespace WalletWasabi.Backend.Controllers
}
// Make sure Alice checks work.
- var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutputHashHex);
+ var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutputScriptHex);
foreach (Guid aliceToRemove in alicesToRemove)
{
@@ -232,13 +232,13 @@ namespace WalletWasabi.Backend.Controllers
byte[] blindedData;
try
{
- blindedData = ByteHelpers.FromHex(request.BlindedOutputHashHex);
+ blindedData = ByteHelpers.FromHex(request.BlindedOutputScriptHex);
}
catch
{
return BadRequest("Invalid blinded output hex.");
}
- Logger.LogDebug<ChaumianCoinJoinController>($"Blinded data hex: {request.BlindedOutputHashHex}");
+ Logger.LogDebug<ChaumianCoinJoinController>($"Blinded data hex: {request.BlindedOutputScriptHex}");
Logger.LogDebug<ChaumianCoinJoinController>($"Blinded data array size: {blindedData.Length}");
byte[] signature = RsaKey.SignBlindedData(blindedData);
@@ -283,13 +283,15 @@ namespace WalletWasabi.Backend.Controllers
/// <returns>RoundHash if the phase is already ConnectionConfirmation.</returns>
/// <response code="200">RoundHash if the phase is already ConnectionConfirmation.</response>
/// <response code="204">If the phase is InputRegistration and Alice is found.</response>
- /// <response code="400">The provided uniqueId was malformed.</response>
+ /// <response code="400">The provided uniqueId or roundId was malformed.</response>
+ /// <response code="403">Participation can be only confirmed from a Running round's InputRegistration or ConnectionConfirmation phase.</response>
/// <response code="404">If Alice is not found.</response>
[HttpPost("confirmation")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
+ [ProducesResponseType(404)]
public async Task<IActionResult> PostConfirmationAsync([FromQuery]string uniqueId, [FromQuery]long roundId)
{
if (roundId <= 0 || !ModelState.IsValid)
@@ -316,7 +318,13 @@ namespace WalletWasabi.Backend.Controllers
return NotFound("Alice not found.");
}
- switch (round.Phase)
+ if (round.Status != CcjRoundStatus.Running)
+ {
+ return Forbid("Round is not running.");
+ }
+
+ CcjRoundPhase phase = round.Phase;
+ switch (phase)
{
case CcjRoundPhase.InputRegistration:
{
@@ -330,7 +338,6 @@ namespace WalletWasabi.Backend.Controllers
// Progress round if needed.
if (round.AllAlices(AliceState.ConnectionConfirmed))
{
-
IEnumerable<Alice> alicesToBan = await round.RemoveAlicesIfInputsSpentAsync(); // So ban only those who confirmed participation, yet spent their inputs.
if (alicesToBan.Count() > 0)
@@ -351,11 +358,11 @@ namespace WalletWasabi.Backend.Controllers
}
}
- return Ok(round.RoundHash);
+ return Ok(round.RoundHash); // Participation can be confirmed multiple times, whatever.
}
- default: // This should never happen?
+ default:
{
- return NotFound();
+ return Forbid($"Participation can be only confirmed from InputRegistration or ConnectionConfirmation phase. Current phase: {phase}.");
}
}
}
@@ -367,8 +374,8 @@ namespace WalletWasabi.Backend.Controllers
/// <param name="roundId">Round identifier, obtained previously.</param>
/// <response code="200">Alice or the round was not found.</response>
/// <response code="204">Alice sucessfully uncofirmed her participation.</response>
- /// <response code="400">The provided uniqueId was malformed.</response>
- /// <response code="403">Participation can be only unconfirmed from InputRegistration phase.</response>
+ /// <response code="400">The provided uniqueId or roundId was malformed.</response>
+ /// <response code="403">Participation can be only unconfirmed from a Running round's InputRegistration phase.</response>
[HttpPost("unconfirmation")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
@@ -400,6 +407,11 @@ namespace WalletWasabi.Backend.Controllers
return Ok("Alice not found.");
}
+ if(round.Status != CcjRoundStatus.Running)
+ {
+ return Forbid("Round is not running.");
+ }
+
CcjRoundPhase phase = round.Phase;
switch (phase)
{
@@ -415,6 +427,81 @@ namespace WalletWasabi.Backend.Controllers
}
}
+ private static AsyncLock OutputLock { get; } = new AsyncLock();
+
+ /// <summary>
+ /// Bob registers his output.
+ /// </summary>
+ /// <param name="roundHash">Hash of the round, obtained previously.</param>
+ /// <returns>RoundHash if the phase is already ConnectionConfirmation.</returns>
+ /// <response code="204">Output is successfully registered.</response>
+ /// <response code="400">The provided roundHash or outpurRequest was malformed.</response>
+ /// <response code="403">Output registration can only be done from a Running rounds's OutputRegistration phase.</response>
+ /// <response code="404">If round not found.</response>
+ [HttpPost("output")]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(403)]
+ [ProducesResponseType(404)]
+ public async Task<IActionResult> PostOutputAsync([FromQuery]string roundHash, [FromBody]OutputRequest outputRequest)
+ {
+ if (string.IsNullOrWhiteSpace(roundHash)
+ || outputRequest == null
+ || string.IsNullOrWhiteSpace(outputRequest.OutputScript)
+ || string.IsNullOrWhiteSpace(outputRequest.SignatureHex)
+ || !ModelState.IsValid)
+ {
+ return BadRequest();
+ }
+
+ CcjRound round = Coordinator.TryGetRound(roundHash);
+ if (round == null)
+ {
+ return NotFound("Round not found.");
+ }
+
+ if (round.Status != CcjRoundStatus.Running)
+ {
+ return Forbid("Round is not running.");
+ }
+
+ CcjRoundPhase phase = round.Phase;
+ if (phase != CcjRoundPhase.OutputRegistration)
+ {
+ return Forbid($"Output registration can only be done from OutputRegistration phase. Current phase: {phase}.");
+ }
+
+ var outputScript = new Script(outputRequest.OutputScript);
+
+ if (RsaKey.PubKey.Verify(ByteHelpers.FromHex(outputRequest.SignatureHex), outputScript.ToBytes()))
+ {
+ using (await OutputLock.LockAsync())
+ {
+ Bob bob = null;
+ try
+ {
+ bob = new Bob(outputScript);
+ round.AddBob(bob);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid outputScript is provided. Details: {ex.Message}");
+ }
+
+ if (round.CountBobs() == round.AnonymitySet)
+ {
+ await round.ExecuteNextPhaseAsync(CcjRoundPhase.Signing);
+ }
+ }
+
+ return NoContent();
+ }
+ else
+ {
+ return BadRequest("Invalid signature provided.");
+ }
+ }
+
/// <summary>
/// Alice asks for the final CoinJoin transaction.
/// </summary>
@@ -422,7 +509,7 @@ namespace WalletWasabi.Backend.Controllers
/// <param name="roundId">Round identifier, obtained previously.</param>
/// <returns>The coinjoin Transaction.</returns>
/// <response code="200">Returns the coinjoin transaction.</response>
- /// <response code="400">The provided uniqueId was malformed.</response>
+ /// <response code="400">The provided uniqueId or roundId was malformed.</response>
[HttpGet("coinjoin/{uniqueId}")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
@@ -442,22 +529,13 @@ namespace WalletWasabi.Backend.Controllers
return Ok();
}
- /// <summary>
- /// Bob registers his output.
- /// </summary>
- /// <param name="roundHash">Hash of the round, obtained previously.</param>
- [HttpPost("output")]
- [ProducesResponseType(204)]
- public IActionResult PostOutput([FromQuery]string roundHash)
- {
- return NoContent();
- }
-
/// <summary>
/// Alice posts her partial signatures.
/// </summary>
+ /// <response code="400">The provided uniqueId or roundId was malformed.</response>
[HttpPost("signatures")]
[ProducesResponseType(204)]
+ [ProducesResponseType(400)]
public IActionResult PostSignatures([FromQuery]string uniqueId, [FromQuery]long roundId)
{
if (roundId <= 0 || !ModelState.IsValid)

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1501,7 +1501,7 @@ namespace WalletWasabi.Tests
// Inputs request tests
var request = new InputsRequest
{
- BlindedOutputHashHex = null,
+ BlindedOutputScriptHex = null,
ChangeOutputScript = null,
Inputs = null,
};
@@ -1512,7 +1512,7 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Invalid request.\"", message);
}
- request.BlindedOutputHashHex = "";
+ request.BlindedOutputScriptHex = "";
request.ChangeOutputScript = "";
request.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(), Proof = "" } };
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
@@ -1522,7 +1522,7 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Invalid request.\"", message);
}
- request.BlindedOutputHashHex = "c";
+ request.BlindedOutputScriptHex = "c";
request.ChangeOutputScript = "a";
request.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(uint256.One, 0), Proof = "b" } };
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
@@ -1600,8 +1600,8 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Provided proof is invalid.\"", message);
}
- request.BlindedOutputHashHex = new Transaction().ToHex();
- proof = key.SignMessage(request.BlindedOutputHashHex);
+ request.BlindedOutputScriptHex = new Transaction().ToHex();
+ proof = key.SignMessage(request.BlindedOutputScriptHex);
request.Inputs.First().Proof = proof;
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
{
@@ -1678,8 +1678,8 @@ namespace WalletWasabi.Tests
Assert.Equal(1, roundState.RegisteredPeerCount);
}
- request.BlindedOutputHashHex = "foo";
- proof = key.SignMessage(request.BlindedOutputHashHex);
+ request.BlindedOutputScriptHex = "foo";
+ proof = key.SignMessage(request.BlindedOutputScriptHex);
request.Inputs.First().Proof = proof;
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
{
@@ -1699,10 +1699,10 @@ namespace WalletWasabi.Tests
}
var blindingKey = Global.Coordinator.RsaKey;
- byte[] scriptHash = HashHelpers.GenerateSha256Hash(key.ScriptPubKey.ToBytes());
- var (BlindingFactor, BlindedData) = blindingKey.PubKey.Blind(scriptHash);
- request.BlindedOutputHashHex = ByteHelpers.ToHex(BlindedData);
- proof = key.SignMessage(request.BlindedOutputHashHex);
+ byte[] scriptBytes = key.ScriptPubKey.ToBytes();
+ var (BlindingFactor, BlindedData) = blindingKey.PubKey.Blind(scriptBytes);
+ request.BlindedOutputScriptHex = ByteHelpers.ToHex(BlindedData);
+ proof = key.SignMessage(request.BlindedOutputScriptHex);
request.Inputs.First().Proof = proof;
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
{
@@ -1727,8 +1727,8 @@ namespace WalletWasabi.Tests
Assert.Equal(1, roundState.RegisteredPeerCount);
}
- request.BlindedOutputHashHex = new Transaction().ToHex();
- proof = key.SignMessage(request.BlindedOutputHashHex);
+ request.BlindedOutputScriptHex = new Transaction().ToHex();
+ proof = key.SignMessage(request.BlindedOutputScriptHex);
request.Inputs.First().Proof = proof;
request.Inputs = new List<InputProofModel> { request.Inputs.First(), request.Inputs.First() };
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
@@ -1755,7 +1755,7 @@ namespace WalletWasabi.Tests
index = i;
}
}
- proof = key.SignMessage(request.BlindedOutputHashHex);
+ proof = key.SignMessage(request.BlindedOutputScriptHex);
inputProofs.Add(new InputProofModel { Input = new OutPoint(hash, index), Proof = proof });
}
await rpc.GenerateAsync(1);

MODIFY
WalletWasabi/Backend/Models/Requests/InputsRequest.cs
WalletWasabi/Backend/Models/Requests/InputsRequest.cs
@@ -15,7 +15,7 @@ namespace WalletWasabi.Backend.Models.Requests
public IEnumerable<InputProofModel> Inputs { get; set; }
[Required]
- public string BlindedOutputHashHex { get; set; }
+ public string BlindedOutputScriptHex { get; set; }
[Required]
public string ChangeOutputScript { get; set; }

MODIFY
WalletWasabi/ChaumianCoinJoin/Alice.cs
WalletWasabi/ChaumianCoinJoin/Alice.cs
@@ -23,7 +23,7 @@ namespace WalletWasabi.ChaumianCoinJoin
public Script ChangeOutputScript { get; }
- public string BlindedOutputHex { get; }
+ public string BlindedOutputScriptHex { get; }
public Money GetChangeAmount(Money denomination, Money coordinatorFee) => OutputSumWithoutCoordinatorFeeAndDenomination - denomination - coordinatorFee;
@@ -33,7 +33,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
Inputs = Guard.NotNullOrEmpty(nameof(inputs), inputs);
NetworkFeeToPay = Guard.NotNull(nameof(networkFeeToPay), networkFeeToPay);
- BlindedOutputHex = Guard.NotNullOrEmptyOrWhitespace(nameof(blindedOutputHex), blindedOutputHex);
+ BlindedOutputScriptHex = Guard.NotNullOrEmptyOrWhitespace(nameof(blindedOutputHex), blindedOutputHex);
Guard.NotNull(nameof(changeOutputScript), changeOutputScript);
// 33 bytes maximum: https://bitcoin.stackexchange.com/a/46379/26859

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
@@ -450,7 +450,7 @@ namespace WalletWasabi.ChaumianCoinJoin
}
}
- public bool ContainsBlindedOutput(string blindedOutputHex, out List<Alice> alices)
+ public bool ContainsBlindedOutputScriptHex(string blindedOutputScriptHex, out List<Alice> alices)
{
alices = new List<Alice>();
@@ -458,7 +458,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
foreach (Alice alice in Alices)
{
- if (alice.BlindedOutputHex == blindedOutputHex)
+ if (alice.BlindedOutputScriptHex == blindedOutputScriptHex)
{
alices.Add(alice);
}
@@ -611,6 +611,25 @@ namespace WalletWasabi.ChaumianCoinJoin
Logger.LogInfo<CcjRound>($"Round ({RoundId}): Alice ({alice.UniqueId}) added.");
}
+ public void AddBob(Bob bob)
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ if (Phase != CcjRoundPhase.OutputRegistration || Status != CcjRoundStatus.Running)
+ {
+ throw new InvalidOperationException("Adding Bob is only allowed in OutputRegistration phase.");
+ }
+ if(Bobs.Any(x=>x.ActiveOutputScript == bob.ActiveOutputScript))
+ {
+ throw new InvalidOperationException("Bob is already added.");
+ }
+
+ Bobs.Add(bob);
+ }
+
+ Logger.LogInfo<CcjRound>($"Round ({RoundId}): Bob added.");
+ }
+
public int RemoveAlicesBy(AliceState state)
{
int numberOfRemovedAlices = 0;

MODIFY
WalletWasabi/Services/CcjCoordinator.cs
WalletWasabi/Services/CcjCoordinator.cs
@@ -245,6 +245,14 @@ namespace WalletWasabi.Services
}
}
+ public CcjRound TryGetRound(string roundHash)
+ {
+ using (RoundsListLock.Lock())
+ {
+ return Rounds.SingleOrDefault(x => x.RoundHash == roundHash);
+ }
+ }
+
public bool AnyRunningRoundContainsInput(OutPoint input, out List<Alice> alices)
{
using (RoundsListLock.Lock())

Implement POST output