progress input registration tests
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -78,7 +78,7 @@ namespace WalletWasabi.Backend.Controllers
// Validate request.
if (!ModelState.IsValid
|| request == null
- || string.IsNullOrWhiteSpace(request.BlindedOutput)
+ || string.IsNullOrWhiteSpace(request.BlindedOutputHex)
|| string.IsNullOrWhiteSpace(request.ChangeOutputScript)
|| request.Inputs == null
|| request.Inputs.Count() == 0
@@ -96,7 +96,7 @@ namespace WalletWasabi.Backend.Controllers
// Do more checks.
try
{
- if (round.ContainsBlindedOutput(request.BlindedOutput, out List<Alice> _))
+ if (round.ContainsBlindedOutput(request.BlindedOutputHex, out List<Alice> _))
{
return BadRequest("Blinded output has already been registered.");
}
@@ -155,7 +155,7 @@ namespace WalletWasabi.Backend.Controllers
var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(Network);
// Check if proofs are valid.
- bool validProof = address.VerifyMessage(request.BlindedOutput, inputProof.Proof);
+ bool validProof = address.VerifyMessage(request.BlindedOutputHex, inputProof.Proof);
if (!validProof)
{
return BadRequest("Provided proof is invalid.");
@@ -174,7 +174,7 @@ namespace WalletWasabi.Backend.Controllers
}
// Make sure Alice checks work.
- var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutput);
+ var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutputHex);
foreach (Guid aliceToRemove in alicesToRemove)
{
@@ -183,7 +183,16 @@ namespace WalletWasabi.Backend.Controllers
round.AddAlice(alice);
// All checks are good. Sign.
- byte[] signature = RsaKey.SignBlindedData(ByteHelpers.FromHex(request.BlindedOutput));
+ byte[] blindedData;
+ try
+ {
+ blindedData = ByteHelpers.FromHex(request.BlindedOutputHex);
+ }
+ catch
+ {
+ return BadRequest("Invalid blinded output hex.");
+ }
+ byte[] signature = RsaKey.SignBlindedData(blindedData);
// Check if phase changed since.
if (round.Status != CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration)

MODIFY
WalletWasabi.Backend/README.md
WalletWasabi.Backend/README.md
| API | Description | Request | Response |
| --- | ---- | ---- | ---- |
| GET status | Satoshi gets various status information. | | CurrentPhase, Denomination, RegisteredPeerCount, RequiredPeerCount, ForcedRoundStartMinutesLeft, MaximumInputCountPerPeer, FeePerInputs, FeePerOutputs, CoordinatorFee, Version |
-| POST inputs | Alice registers her inputs. | Inputs[(Input, Proof)], BlindedOutput, ChangeOutputs[] | SignedBlindedOutput, UniqueId |
+| POST inputs | Alice registers her inputs. | Inputs[(Input, Proof)], BlindedOutputHex, ChangeOutputs[] | SignedBlindedOutput, UniqueId |
| POST confirmation | Alice must confirm her participation periodically in InputRegistration phase and confirm once in ConnectionConfirmation phase. | UniqueId | RoundHash if the phase is already ConnectionConfirmation. |
| POST unconfirmation | Alice can revoke her registration without penalty if the current phase is InputRegistration. | UniqueId | |
| POST outputs | Bob registers his output. | Output, Signature, RoundHash | |

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -31,6 +31,7 @@ using Xunit;
using WalletWasabi.Backend.Models.Responses;
using WalletWasabi.ChaumianCoinJoin;
using WalletWasabi.Backend.Models.Requests;
+using WalletWasabi.Crypto;
namespace WalletWasabi.Tests
{
@@ -1252,7 +1253,7 @@ namespace WalletWasabi.Tests
}
catch
{
- // Magic swallow
+ // Magic swallow.
}
await Global.RpcClient.GenerateAsync(10);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 10);
@@ -1474,7 +1475,7 @@ namespace WalletWasabi.Tests
// Inputs request tests
var request = new InputsRequest
{
- BlindedOutput = null,
+ BlindedOutputHex = null,
ChangeOutputScript = null,
Inputs = null,
};
@@ -1485,7 +1486,7 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Invalid request.\"", message);
}
- request.BlindedOutput = "";
+ request.BlindedOutputHex = "";
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()))
@@ -1495,7 +1496,7 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Invalid request.\"", message);
}
- request.BlindedOutput = "c";
+ request.BlindedOutputHex = "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()))
@@ -1573,8 +1574,8 @@ namespace WalletWasabi.Tests
Assert.Equal("\"Provided proof is invalid.\"", message);
}
- request.BlindedOutput = new Transaction().ToHex();
- proof = key.SignMessage(request.BlindedOutput);
+ request.BlindedOutputHex = new Transaction().ToHex();
+ proof = key.SignMessage(request.BlindedOutputHex);
request.Inputs.First().Proof = proof;
using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
{
@@ -1582,6 +1583,101 @@ namespace WalletWasabi.Tests
string message = await response.Content.ReadAsStringAsync();
Assert.StartsWith("\"Not enough inputs are provided. Fee to pay:", message);
}
+
+ roundConfig.Denomination = new Money(0.01m, MoneyUnit.BTC); // exactly the same as our output
+ coordinator.UpdateRoundConfig(roundConfig);
+ coordinator.FailAllRoundsInInputRegistration();
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string message = await response.Content.ReadAsStringAsync();
+ Assert.StartsWith("\"Not enough inputs are provided. Fee to pay:", message);
+ }
+
+ roundConfig.Denomination = new Money(0.00999999m, MoneyUnit.BTC); // one satoshi less than our output
+ coordinator.UpdateRoundConfig(roundConfig);
+ coordinator.FailAllRoundsInInputRegistration();
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string message = await response.Content.ReadAsStringAsync();
+ Assert.StartsWith("\"Not enough inputs are provided. Fee to pay:", message);
+ }
+
+ roundConfig.Denomination = new Money(0.008m, MoneyUnit.BTC); // one satoshi less than our output
+ coordinator.UpdateRoundConfig(roundConfig);
+ coordinator.FailAllRoundsInInputRegistration();
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var inputsResp = await response.Content.ReadAsJsonAsync<InputsResponse>();
+ Assert.NotNull(inputsResp.BlindedOutputSignature);
+ Assert.NotEqual(Guid.Empty, inputsResp.UniqueId);
+ }
+
+ using (var response = await client.SendAsync(HttpMethod.Get, "/api/v1/btc/chaumiancoinjoin/status/"))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var status = await response.Content.ReadAsJsonAsync<CcjStatusResponse>();
+ Assert.Equal(CcjRoundPhase.InputRegistration, status.CurrentPhase);
+ Assert.Equal(1, status.RegisteredPeerCount);
+ }
+
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string message = await response.Content.ReadAsStringAsync();
+ Assert.Equal("\"Blinded output has already been registered.\"", message);
+ }
+
+ using (var response = await client.SendAsync(HttpMethod.Get, "/api/v1/btc/chaumiancoinjoin/status/"))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var status = await response.Content.ReadAsJsonAsync<CcjStatusResponse>();
+ Assert.Equal(CcjRoundPhase.InputRegistration, status.CurrentPhase);
+ Assert.Equal(1, status.RegisteredPeerCount);
+ }
+
+ request.BlindedOutputHex = "foo";
+ proof = key.SignMessage(request.BlindedOutputHex);
+ request.Inputs.First().Proof = proof;
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string message = await response.Content.ReadAsStringAsync();
+ Assert.Equal("\"Invalid blinded output hex.\"", message);
+ }
+
+ using (var response = await client.SendAsync(HttpMethod.Get, "/api/v1/btc/chaumiancoinjoin/status/"))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var status = await response.Content.ReadAsJsonAsync<CcjStatusResponse>();
+ Assert.Equal(CcjRoundPhase.InputRegistration, status.CurrentPhase);
+ Assert.Equal(1, status.RegisteredPeerCount);
+ }
+
+ var blindingKey = new BlindingRsaKey();
+ string script = key.ScriptPubKey.ToString();
+ byte[] messageToBlind = Encoding.UTF8.GetBytes(script);
+ var (BlindingFactor, BlindedData) = blindingKey.PubKey.Blind(messageToBlind);
+ request.BlindedOutputHex = ByteHelpers.ToHex(BlindedData);
+ proof = key.SignMessage(request.BlindedOutputHex);
+ request.Inputs.First().Proof = proof;
+ using (var response = await client.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var inputsResp = await response.Content.ReadAsJsonAsync<InputsResponse>();
+ Assert.NotNull(inputsResp.BlindedOutputSignature);
+ Assert.NotEqual(Guid.Empty, inputsResp.UniqueId);
+ }
+
+ using (var response = await client.SendAsync(HttpMethod.Get, "/api/v1/btc/chaumiancoinjoin/status/"))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var status = await response.Content.ReadAsJsonAsync<CcjStatusResponse>();
+ Assert.Equal(CcjRoundPhase.InputRegistration, status.CurrentPhase);
+ Assert.Equal(1, status.RegisteredPeerCount);
+ }
}
}

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 BlindedOutput { get; set; }
+ public string BlindedOutputHex { get; set; }
[Required]
public string ChangeOutputScript { get; set; }

MODIFY
WalletWasabi/ChaumianCoinJoin/Alice.cs
WalletWasabi/ChaumianCoinJoin/Alice.cs
@@ -23,17 +23,17 @@ namespace WalletWasabi.ChaumianCoinJoin
public Script ChangeOutputScript { get; }
- public string BlindedOutput { get; }
+ public string BlindedOutputHex { get; }
public Money GetChangeAmount(Money denomination, Money coordinatorFee) => OutputSumWithoutCoordinatorFeeAndDenomination - denomination - coordinatorFee;
public AliceState State { get; set; }
- public Alice(IEnumerable<(OutPoint OutPoint, TxOut Output)> inputs, Money networkFeeToPay, Script changeOutputScript, string blindedOutput)
+ public Alice(IEnumerable<(OutPoint OutPoint, TxOut Output)> inputs, Money networkFeeToPay, Script changeOutputScript, string blindedOutputHex)
{
Inputs = Guard.NotNullOrEmpty(nameof(inputs), inputs);
NetworkFeeToPay = Guard.NotNull(nameof(networkFeeToPay), networkFeeToPay);
- BlindedOutput = Guard.NotNullOrEmptyOrWhitespace(nameof(blindedOutput), blindedOutput);
+ BlindedOutputHex = 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
@@ -395,7 +395,7 @@ namespace WalletWasabi.ChaumianCoinJoin
}
}
- public bool ContainsBlindedOutput(string blindedOutput, out List<Alice> alices)
+ public bool ContainsBlindedOutput(string blindedOutputHex, out List<Alice> alices)
{
alices = new List<Alice>();
@@ -403,7 +403,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
foreach (Alice alice in Alices)
{
- if (alice.BlindedOutput == blindedOutput)
+ if (alice.BlindedOutputHex == blindedOutputHex)
{
alices.Add(alice);
}
@@ -513,7 +513,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
using (RoundSyncronizerLock.Lock())
{
- if (Phase != CcjRoundPhase.InputRegistration || Phase != CcjRoundPhase.ConnectionConfirmation)
+ if (Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation || Status != CcjRoundStatus.Running)
{
throw new InvalidOperationException("Updating anonymity set is only allowed in InputRegistration and ConnectionConfirmation phases.");
}
@@ -525,7 +525,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
using (RoundSyncronizerLock.Lock())
{
- if (Phase != CcjRoundPhase.InputRegistration)
+ if (Phase != CcjRoundPhase.InputRegistration || Status != CcjRoundStatus.Running)
{
throw new InvalidOperationException("Adding Alice is only allowed in InputRegistration phase.");
}
@@ -539,7 +539,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
using (RoundSyncronizerLock.Lock())
{
- if (Phase != CcjRoundPhase.InputRegistration || Phase != CcjRoundPhase.ConnectionConfirmation)
+ if (Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation || Status != CcjRoundStatus.Running)
{
throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
}
@@ -551,7 +551,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
using (RoundSyncronizerLock.Lock())
{
- if (Phase != CcjRoundPhase.InputRegistration || Phase != CcjRoundPhase.ConnectionConfirmation)
+ if (Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation || Status != CcjRoundStatus.Running)
{
throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
}
@@ -563,7 +563,7 @@ namespace WalletWasabi.ChaumianCoinJoin
{
using (RoundSyncronizerLock.Lock())
{
- if (Phase != CcjRoundPhase.InputRegistration || Phase != CcjRoundPhase.ConnectionConfirmation)
+ if (Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation || Status != CcjRoundStatus.Running)
{
throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
}

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRoundConfig.cs
WalletWasabi/ChaumianCoinJoin/CcjRoundConfig.cs
@@ -17,32 +17,32 @@ namespace WalletWasabi.ChaumianCoinJoin
public class CcjRoundConfig : IConfig
{
/// <inheritdoc />
- public string FilePath { get; private set; }
+ public string FilePath { get; internal set; }
[JsonProperty(PropertyName = "Denomination")]
[JsonConverter(typeof(MoneyBtcJsonConverter))]
- public Money Denomination { get; private set; }
+ public Money Denomination { get; internal set; }
[JsonProperty(PropertyName = "ConfirmationTarget")]
- public int? ConfirmationTarget { get; private set; }
+ public int? ConfirmationTarget { get; internal set; }
[JsonProperty(PropertyName = "CoordinatorFeePercent")]
- public decimal? CoordinatorFeePercent { get; private set; }
+ public decimal? CoordinatorFeePercent { get; internal set; }
[JsonProperty(PropertyName = "AnonymitySet")]
- public int? AnonymitySet { get; private set; }
+ public int? AnonymitySet { get; internal set; }
[JsonProperty(PropertyName = "InputRegistrationTimeout")]
- public long? InputRegistrationTimeout { get; private set; }
+ public long? InputRegistrationTimeout { get; internal set; }
[JsonProperty(PropertyName = "ConnectionConfirmationTimeout")]
- public long? ConnectionConfirmationTimeout { get; private set; }
+ public long? ConnectionConfirmationTimeout { get; internal set; }
[JsonProperty(PropertyName = "OutputRegistrationTimeout")]
- public long? OutputRegistrationTimeout { get; private set; }
+ public long? OutputRegistrationTimeout { get; internal set; }
[JsonProperty(PropertyName = "SigningTimeout")]
- public long? SigningTimeout { get; private set; }
+ public long? SigningTimeout { get; internal set; }
public CcjRoundConfig()
{

progress input registration tests