Add backend logic
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -21,6 +21,7 @@ using WalletWasabi.Logging;
using WalletWasabi.Models;
using WalletWasabi.Models.ChaumianCoinJoin;
using WalletWasabi.Services;
+using static NBitcoin.Crypto.ECDSABlinding;
namespace WalletWasabi.Backend.Controllers
{
@@ -61,6 +62,7 @@ namespace WalletWasabi.Backend.Controllers
{
Phase = round.Phase,
SignerPubKey = round.Signer.Key.PubKey,
+ AdditionalSignerPubKeys = round.AdditionalMixingLevels.Select(x => x.signer.Key.PubKey),
RpubKey = round.Rkey.PubKey,
Denomination = round.Denomination,
RegisteredPeerCount = round.CountAlices(syncLock: false),
@@ -106,6 +108,7 @@ namespace WalletWasabi.Backend.Controllers
{
// Validate request.
if (!ModelState.IsValid
+ || !request.AdditionalBlindedOutputScripts.Any()
|| !request.Inputs.Any()
|| request.Inputs.Any(x => x.Input == default(TxoRef)
|| x.Input.TransactionId is null))
@@ -125,7 +128,22 @@ namespace WalletWasabi.Backend.Controllers
// Do more checks.
try
{
- if (round.ContainsBlindedOutputScript(request.BlindedOutputScript, out _))
+ var additionalBlindedOutputScriptArray = request.AdditionalBlindedOutputScripts.ToArray();
+ int additionalBlindedOutputCount = additionalBlindedOutputScriptArray.Length;
+ int maximumAdditionalBlindedOutputCount = round.AdditionalMixingLevels.Length;
+ if (additionalBlindedOutputCount > maximumAdditionalBlindedOutputCount)
+ {
+ return BadRequest($"Too many blinded output was provided: {additionalBlindedOutputCount}, maximum: {maximumAdditionalBlindedOutputCount}.");
+ }
+
+ IEnumerable<uint256> allBlindedOutputScript = additionalBlindedOutputScriptArray.Concat(new uint256[] { request.BlindedOutputScript });
+
+ if (allBlindedOutputScript.Distinct().Count() < allBlindedOutputScript.Count())
+ {
+ return BadRequest("Duplicate blinded output found.");
+ }
+
+ if (round.ContainsAnyBlindedOutputScript(allBlindedOutputScript, out _))
{
return BadRequest("Blinded output has already been registered.");
}
@@ -231,8 +249,22 @@ namespace WalletWasabi.Backend.Controllers
return BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPay.ToString(false, true)} BTC. Round denomination: {round.Denomination.ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC.");
}
+ // Make sure we sign the proper number of additional blinded outputs.
+ var moneySoFar = Money.Zero;
+ var acceptedAdditionalBlindedOutputScripts = new List<uint256>();
+ for (int i = 0; i < additionalBlindedOutputCount; i++)
+ {
+ (Money denomination, Signer signer) mixingLevel = round.AdditionalMixingLevels[i];
+ changeAmount -= mixingLevel.denomination - round.FeePerOutputs - mixingLevel.denomination.Percentange(round.CoordinatorFeePercent) * round.AnonymitySet; // It should be the number of bobs, but we must make sure they'd have money to pay all.
+
+ if (changeAmount >= Money.Zero)
+ {
+ acceptedAdditionalBlindedOutputScripts.Add(additionalBlindedOutputScriptArray[i]);
+ }
+ }
+
// Make sure Alice checks work.
- var alice = new Alice(inputs, networkFeeToPay, request.ChangeOutputAddress, request.BlindedOutputScript);
+ var alice = new Alice(inputs, networkFeeToPay, request.ChangeOutputAddress, request.BlindedOutputScript, acceptedAdditionalBlindedOutputScripts);
foreach (Guid aliceToRemove in alicesToRemove)
{
@@ -242,6 +274,13 @@ namespace WalletWasabi.Backend.Controllers
// All checks are good. Sign.
BigInteger blindSignature = round.Signer.Sign(request.BlindedOutputScript);
+ var additionalBlindSignatures = new List<BigInteger>();
+ for (int i = 0; i < acceptedAdditionalBlindedOutputScripts.Count; i++)
+ {
+ (Money denomination, Signer signer) mixingLevel = round.AdditionalMixingLevels[i];
+ var bs = mixingLevel.signer.Sign(acceptedAdditionalBlindedOutputScripts[i]);
+ additionalBlindSignatures.Add(bs);
+ }
// Check if phase changed since.
if (round.Status != CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration)
@@ -264,6 +303,7 @@ namespace WalletWasabi.Backend.Controllers
{
UniqueId = alice.UniqueId,
BlindedOutputSignature = blindSignature,
+ AdditionalBlindedOutputSignatures = additionalBlindSignatures,
RoundId = round.RoundId
};
return Ok(resp);
@@ -430,6 +470,7 @@ namespace WalletWasabi.Backend.Controllers
public async Task<IActionResult> PostOutputAsync([FromQuery, Required]long roundId, [FromBody, Required]OutputRequest request)
{
if (roundId < 0
+ || request.Level < 0
|| !ModelState.IsValid)
{
return BadRequest();
@@ -461,14 +502,29 @@ namespace WalletWasabi.Backend.Controllers
}
}
- if (round.Signer.VerifyUnblindedSignature(request.UnblindedSignature, request.OutputAddress.ScriptPubKey.ToBytes()))
+ if (request.Level > round.AdditionalMixingLevels.Length)
+ {
+ return BadRequest($"Invalid mixing Level is provided. Provided: {request.Level}. Maximum: {round.AdditionalMixingLevels.Length}.");
+ }
+
+ Signer signer;
+ if (request.Level == 0)
+ {
+ signer = round.Signer;
+ }
+ else
+ {
+ signer = round.AdditionalMixingLevels[request.Level - 1].signer;
+ }
+
+ if (signer.VerifyUnblindedSignature(request.UnblindedSignature, request.OutputAddress.ScriptPubKey.ToBytes()))
{
using (await OutputLock.LockAsync())
{
Bob bob = null;
try
{
- bob = new Bob(request.OutputAddress);
+ bob = new Bob(request.OutputAddress, request.Level);
round.AddBob(bob);
}
catch (Exception ex)

MODIFY
WalletWasabi/Backend/Models/Requests/InputsRequest.cs
WalletWasabi/Backend/Models/Requests/InputsRequest.cs
@@ -17,6 +17,10 @@ namespace WalletWasabi.Backend.Models.Requests
[JsonConverter(typeof(Uint256JsonConverter))]
public uint256 BlindedOutputScript { get; set; }
+ [Required]
+ [JsonConverter(typeof(IEnumerable<Uint256JsonConverter>))]
+ public IEnumerable<uint256> AdditionalBlindedOutputScripts { get; set; }
+
[Required]
[JsonConverter(typeof(BitcoinAddressJsonConverter))]
public BitcoinAddress ChangeOutputAddress { get; set; }

MODIFY
WalletWasabi/Backend/Models/Requests/OutputRequest.cs
WalletWasabi/Backend/Models/Requests/OutputRequest.cs
@@ -14,6 +14,9 @@ namespace WalletWasabi.Backend.Models.Requests
[JsonConverter(typeof(BitcoinAddressJsonConverter))]
public BitcoinAddress OutputAddress { get; set; }
+ [Required]
+ public int Level { get; set; }
+
[Required]
[JsonConverter(typeof(BlindSignatureJsonConverter))]
public BlindSignature UnblindedSignature { get; set; }

MODIFY
WalletWasabi/Backend/Models/Responses/CcjRunningRoundState.cs
WalletWasabi/Backend/Models/Responses/CcjRunningRoundState.cs
@@ -19,6 +19,9 @@ namespace WalletWasabi.Backend.Models.Responses
[JsonConverter(typeof(PubKeyJsonConverter))]
public PubKey SignerPubKey { get; set; }
+ [JsonConverter(typeof(PubKeyJsonConverter))]
+ public IEnumerable<PubKey> AdditionalSignerPubKeys { get; set; }
+
[JsonConverter(typeof(PubKeyJsonConverter))]
public PubKey RpubKey { get; set; }
@@ -52,6 +55,7 @@ namespace WalletWasabi.Backend.Models.Responses
Phase = state.Phase,
Denomination = state.Denomination,
SignerPubKey = state.SignerPubKey,
+ AdditionalSignerPubKeys = state.AdditionalSignerPubKeys,
RpubKey = state.RpubKey,
RegisteredPeerCount = registeredPeerCount,
RequiredPeerCount = state.RequiredPeerCount,

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/Alice.cs
WalletWasabi/Models/ChaumianCoinJoin/Alice.cs
@@ -16,24 +16,24 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
public Money NetworkFeeToPay { get; }
- public Money OutputSumWithoutCoordinatorFeeAndDenomination { get; }
-
public IEnumerable<Coin> Inputs { get; }
public BitcoinAddress ChangeOutputAddress { get; }
public uint256 BlindedOutputScript { get; }
- public Money GetChangeAmount(Money denomination, Money coordinatorFee) => OutputSumWithoutCoordinatorFeeAndDenomination - denomination - coordinatorFee;
-
public AliceState State { get; set; }
- public Alice(IEnumerable<Coin> inputs, Money networkFeeToPay, BitcoinAddress changeOutputAddress, uint256 blindedOutputScript)
+ public uint256[] AdditionalBlindedOutputScripts { get; }
+
+ public Alice(IEnumerable<Coin> inputs, Money networkFeeToPay, BitcoinAddress changeOutputAddress, uint256 blindedOutputScript, IEnumerable<uint256> additionalBlindedOutputScripts)
{
Inputs = Guard.NotNullOrEmpty(nameof(inputs), inputs);
NetworkFeeToPay = Guard.NotNull(nameof(networkFeeToPay), networkFeeToPay);
BlindedOutputScript = Guard.NotNull(nameof(blindedOutputScript), blindedOutputScript);
+ AdditionalBlindedOutputScripts = additionalBlindedOutputScripts?.ToArray() ?? new uint256[0];
+
ChangeOutputAddress = Guard.NotNull(nameof(changeOutputAddress), changeOutputAddress);
LastSeen = DateTimeOffset.UtcNow;
@@ -41,8 +41,6 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
InputSum = inputs.Sum(x => x.Amount);
- OutputSumWithoutCoordinatorFeeAndDenomination = InputSum - NetworkFeeToPay;
-
State = AliceState.InputsRegistered;
}
}

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/Bob.cs
WalletWasabi/Models/ChaumianCoinJoin/Bob.cs
@@ -5,11 +5,13 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
{
public class Bob
{
+ public int Level { get; }
public BitcoinAddress ActiveOutputAddress { get; }
- public Bob(BitcoinAddress activeOutputAddress)
+ public Bob(BitcoinAddress activeOutputAddress, int level)
{
ActiveOutputAddress = Guard.NotNull(nameof(activeOutputAddress), activeOutputAddress);
+ Level = Guard.MinimumAndNotNull(nameof(level), level, 0);
}
}
}

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
@@ -37,6 +37,8 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
public Key Rkey { get; }
public Signer Signer { get; }
+ public (Money denomination, Signer signer)[] AdditionalMixingLevels { get; }
+
public string GetUnsignedCoinJoinHex()
{
if (_unsignedCoinJoinHex is null)
@@ -162,6 +164,23 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
Rkey = new Key();
Signer = new Signer(new Key(), Rkey);
+ AdditionalMixingLevels = new (Money denomination, Signer signer)[10];
+ for (int i = 0; i < AdditionalMixingLevels.Length; i++)
+ {
+ var signer = new Signer(new Key(), Rkey);
+ Money denomination;
+ if (i == 0)
+ {
+ denomination = 2 * Denomination;
+ }
+ else
+ {
+ denomination = 2 * AdditionalMixingLevels[i - 1].denomination;
+ }
+
+ AdditionalMixingLevels[i] = (denomination, signer);
+ }
+
_unsignedCoinJoinHex = null;
UnsignedCoinJoin = null;
@@ -246,18 +265,38 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
// Build CoinJoin
// 1. Set new denomination: minor optimization.
- Money newDenomination = Alices.Min(x => x.OutputSumWithoutCoordinatorFeeAndDenomination);
+ Money newDenomination = Alices.Min(x => x.InputSum - x.NetworkFeeToPay);
var transaction = Network.Consensus.ConsensusFactory.CreateTransaction();
// 2. Add Bob outputs.
- foreach (Bob bob in Bobs)
+ foreach (Bob bob in Bobs.Where(x => x.Level == 0))
{
transaction.Outputs.Add(newDenomination, bob.ActiveOutputAddress.ScriptPubKey);
}
+ // 2.1 If the newDenomination equals to the Denomination, then we knew the denomination at
+ // registration time so we can tinker with additional mixing levels.
+ int missingBobCount = Alices.Count - Bobs.Count(x => x.Level == 0);
+ bool tinkerWithAdditionalMixingLevels = missingBobCount == 0 && newDenomination == Denomination;
+ if (tinkerWithAdditionalMixingLevels)
+ {
+ for (int i = 0; i < AdditionalMixingLevels.Length; i++)
+ {
+ (Money denomination, Signer signer) mixingLevel = AdditionalMixingLevels[i];
+ IEnumerable<Bob> bobsOnThisLevel = Bobs.Where(x => x.Level == i + 1);
+ int numberOfBobsOnThisLevel = bobsOnThisLevel.Count();
+ if (numberOfBobsOnThisLevel == 1) break;
+
+ foreach (Bob bob in bobsOnThisLevel)
+ {
+ transaction.Outputs.Add(mixingLevel.denomination, bob.ActiveOutputAddress.ScriptPubKey);
+ }
+ }
+ }
+
BitcoinWitPubKeyAddress coordinatorAddress = Constants.GetCoordinatorAddress(Network);
// 3. If there are less Bobs than Alices, then add our own address. The malicious Alice, who will refuse to sign.
- for (int i = 0; i < Alices.Count - Bobs.Count; i++)
+ for (int i = 0; i < missingBobCount; i++)
{
transaction.Outputs.Add(newDenomination, coordinatorAddress);
}
@@ -266,6 +305,19 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
Money coordinatorFeePerAlice = newDenomination.Percentange(CoordinatorFeePercent) * Alices.Count;
Money coordinatorFee = Alices.Count * coordinatorFeePerAlice;
+ if (tinkerWithAdditionalMixingLevels)
+ {
+ for (int i = 0; i < AdditionalMixingLevels.Length; i++)
+ {
+ (Money denomination, Signer signer) mixingLevel = AdditionalMixingLevels[i];
+ IEnumerable<Bob> bobsOnThisLevel = Bobs.Where(x => x.Level == i + 1);
+ int numberOfBobsOnThisLevel = bobsOnThisLevel.Count();
+ if (numberOfBobsOnThisLevel == 1) break;
+
+ coordinatorFee += mixingLevel.denomination.Percentange(CoordinatorFeePercent) * numberOfBobsOnThisLevel * numberOfBobsOnThisLevel;
+ }
+ }
+
// 5. Add the inputs and the changes of Alices.
var spentCoins = new List<Coin>();
foreach (Alice alice in Alices)
@@ -275,7 +327,22 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
transaction.Inputs.Add(new TxIn(input.Outpoint));
spentCoins.Add(input);
}
- Money changeAmount = alice.GetChangeAmount(newDenomination, coordinatorFeePerAlice);
+
+ Money changeAmount = alice.InputSum - alice.NetworkFeeToPay - newDenomination - coordinatorFeePerAlice;
+
+ if (tinkerWithAdditionalMixingLevels)
+ {
+ for (int i = 0; i < alice.AdditionalBlindedOutputScripts.Length; i++)
+ {
+ (Money denomination, Signer signer) mixingLevel = AdditionalMixingLevels[i];
+ IEnumerable<Bob> bobsOnThisLevel = Bobs.Where(x => x.Level == i + 1);
+ int numberOfBobsOnThisLevel = bobsOnThisLevel.Count();
+ if (numberOfBobsOnThisLevel == 1) break;
+
+ changeAmount -= mixingLevel.denomination - FeePerOutputs - mixingLevel.denomination.Percentange(CoordinatorFeePercent) * numberOfBobsOnThisLevel;
+ }
+ }
+
if (changeAmount > Money.Zero) // If the coordinator fee would make change amount to be negative or zero then no need to pay it.
{
Money minimumOutputAmount = Money.Coins(0.0001m); // If the change would be less than about $1 then add it to the coordinator.
@@ -310,59 +377,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
.BuildTransaction(false);
// 8. Try optimize fees.
- try
- {
- // 8.1. Estimate the current FeeRate. Note, there are no signatures yet!
- int estimatedSigSizeBytes = UnsignedCoinJoin.Inputs.Count * Constants.P2wpkhInputSizeInBytes;
- int estimatedFinalTxSize = UnsignedCoinJoin.GetSerializedSize() + estimatedSigSizeBytes;
- Money fee = UnsignedCoinJoin.GetFee(spentCoins.ToArray());
- // There is a currentFeeRate null check later.
- FeeRate currentFeeRate = fee is null ? null : new FeeRate(fee, estimatedFinalTxSize);
-
- // 8.2. Get the most optimal FeeRate.
- EstimateSmartFeeResponse estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, tryOtherFeeRates: true);
- if (estimateSmartFeeResponse is null) throw new InvalidOperationException("FeeRate is not yet initialized");
- FeeRate optimalFeeRate = estimateSmartFeeResponse.FeeRate;
-
- if (!(optimalFeeRate is null) && optimalFeeRate != FeeRate.Zero && !(currentFeeRate is null) && currentFeeRate != FeeRate.Zero) // This would be really strange if it'd happen.
- {
- var sanityFeeRate = new FeeRate(2m); // 2 s/b
- optimalFeeRate = optimalFeeRate < sanityFeeRate ? sanityFeeRate : optimalFeeRate;
- if (optimalFeeRate < currentFeeRate)
- {
- // 8.2 If the fee can be lowered, lower it.
- // 8.2.1. How much fee can we save?
- Money feeShouldBePaid = new Money(estimatedFinalTxSize * (int)optimalFeeRate.SatoshiPerByte);
- Money toSave = fee - feeShouldBePaid;
-
- // 8.2.2. Get the outputs to divide the savings between.
- int maxMixCount = UnsignedCoinJoin.GetIndistinguishableOutputs().Max(x => x.count);
- Money bestMixAmount = UnsignedCoinJoin.GetIndistinguishableOutputs().Where(x => x.count == maxMixCount).Max(x => x.value);
- int bestMixCount = UnsignedCoinJoin.GetIndistinguishableOutputs().First(x => x.value == bestMixAmount).count;
-
- // 8.2.3. Get the savings per best mix outputs.
- long toSavePerBestMixOutputs = toSave.Satoshi / bestMixCount;
-
- // 8.2.4. Modify the best mix outputs in the transaction.
- if (toSavePerBestMixOutputs > 0)
- {
- foreach (TxOut output in UnsignedCoinJoin.Outputs.Where(x => x.Value == bestMixAmount))
- {
- output.Value += toSavePerBestMixOutputs;
- }
- }
- }
- }
- else
- {
- Logger.LogError<CcjRound>($"This is impossible. {nameof(optimalFeeRate)}: {optimalFeeRate}, {nameof(currentFeeRate)}: {currentFeeRate}.");
- }
- }
- catch (Exception ex)
- {
- Logger.LogWarning<CcjRound>("Couldn't optimize fees. Fallback to normal fees.");
- Logger.LogWarning<CcjRound>(ex);
- }
+ await OptimizeFeesAsync(spentCoins);
SignedCoinJoin = Transaction.Parse(UnsignedCoinJoin.ToHex(), Network);
@@ -523,6 +538,63 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
+ private async Task OptimizeFeesAsync(List<Coin> spentCoins)
+ {
+ try
+ {
+ // 8.1. Estimate the current FeeRate. Note, there are no signatures yet!
+ int estimatedSigSizeBytes = UnsignedCoinJoin.Inputs.Count * Constants.P2wpkhInputSizeInBytes;
+ int estimatedFinalTxSize = UnsignedCoinJoin.GetSerializedSize() + estimatedSigSizeBytes;
+ Money fee = UnsignedCoinJoin.GetFee(spentCoins.ToArray());
+ // There is a currentFeeRate null check later.
+ FeeRate currentFeeRate = fee is null ? null : new FeeRate(fee, estimatedFinalTxSize);
+
+ // 8.2. Get the most optimal FeeRate.
+ EstimateSmartFeeResponse estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, tryOtherFeeRates: true);
+ if (estimateSmartFeeResponse is null) throw new InvalidOperationException("FeeRate is not yet initialized");
+ FeeRate optimalFeeRate = estimateSmartFeeResponse.FeeRate;
+
+ if (!(optimalFeeRate is null) && optimalFeeRate != FeeRate.Zero && !(currentFeeRate is null) && currentFeeRate != FeeRate.Zero) // This would be really strange if it'd happen.
+ {
+ var sanityFeeRate = new FeeRate(2m); // 2 s/b
+ optimalFeeRate = optimalFeeRate < sanityFeeRate ? sanityFeeRate : optimalFeeRate;
+ if (optimalFeeRate < currentFeeRate)
+ {
+ // 8.2 If the fee can be lowered, lower it.
+ // 8.2.1. How much fee can we save?
+ Money feeShouldBePaid = new Money(estimatedFinalTxSize * (int)optimalFeeRate.SatoshiPerByte);
+ Money toSave = fee - feeShouldBePaid;
+
+ // 8.2.2. Get the outputs to divide the savings between.
+ int maxMixCount = UnsignedCoinJoin.GetIndistinguishableOutputs().Max(x => x.count);
+ Money bestMixAmount = UnsignedCoinJoin.GetIndistinguishableOutputs().Where(x => x.count == maxMixCount).Max(x => x.value);
+ int bestMixCount = UnsignedCoinJoin.GetIndistinguishableOutputs().First(x => x.value == bestMixAmount).count;
+
+ // 8.2.3. Get the savings per best mix outputs.
+ long toSavePerBestMixOutputs = toSave.Satoshi / bestMixCount;
+
+ // 8.2.4. Modify the best mix outputs in the transaction.
+ if (toSavePerBestMixOutputs > 0)
+ {
+ foreach (TxOut output in UnsignedCoinJoin.Outputs.Where(x => x.Value == bestMixAmount))
+ {
+ output.Value += toSavePerBestMixOutputs;
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogError<CcjRound>($"This is impossible. {nameof(optimalFeeRate)}: {optimalFeeRate}, {nameof(currentFeeRate)}: {currentFeeRate}.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning<CcjRound>("Couldn't optimize fees. Fallback to normal fees.");
+ Logger.LogWarning<CcjRound>(ex);
+ }
+ }
+
public static async Task<(Money feePerInputs, Money feePerOutputs)> CalculateFeesAsync(RPCClient rpc, int confirmationTarget)
{
Guard.NotNull(nameof(rpc), rpc);
@@ -641,6 +713,24 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
return alices.Count > 0;
}
+ public bool ContainsAnyBlindedOutputScript(IEnumerable<uint256> blindedOutputScripts, out List<Alice> alices)
+ {
+ alices = new List<Alice>();
+
+ using (RoundSynchronizerLock.Lock())
+ {
+ foreach (Alice alice in Alices)
+ {
+ if (blindedOutputScripts.Contains(alice.BlindedOutputScript))
+ {
+ alices.Add(alice);
+ }
+ }
+ }
+
+ return alices.Count > 0;
+ }
+
public bool ContainsInput(OutPoint input, out List<Alice> alices)
{
alices = new List<Alice>();

Add backend logic