public static async Task <AliceClient> CreateNewAsync( long roundId, IEnumerable <BitcoinAddress> registeredAddresses, IEnumerable <SchnorrPubKey> schnorrPubKeys, IEnumerable <Requester> requesters, Network network, BitcoinAddress changeOutput, IEnumerable <uint256> blindedOutputScriptHashes, IEnumerable <InputProofModel> inputs, Func <Uri> baseUriAction, EndPoint torSocks5EndPoint) { var request = new InputsRequest { RoundId = roundId, BlindedOutputScripts = blindedOutputScriptHashes, ChangeOutputAddress = changeOutput, Inputs = inputs }; AliceClient client = new AliceClient(roundId, registeredAddresses, schnorrPubKeys, requesters, network, baseUriAction, torSocks5EndPoint); try { // Correct it if forgot to set. if (request.RoundId != roundId) { if (request.RoundId == 0) { request.RoundId = roundId; } else { throw new NotSupportedException($"InputRequest {nameof(roundId)} does not match to the provided {nameof(roundId)}: {request.RoundId} != {roundId}."); } } using HttpResponseMessage response = await client.TorClient.SendAsync(HttpMethod.Post, $"/api/v{WasabiClient.ApiVersion}/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent()).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK) { await response.ThrowRequestExceptionFromContentAsync().ConfigureAwait(false); } var inputsResponse = await response.Content.ReadAsJsonAsync <InputsResponse>().ConfigureAwait(false); if (inputsResponse.RoundId != roundId) // This should never happen. If it does, that's a bug in the coordinator. { throw new NotSupportedException($"Coordinator assigned us to the wrong round: {inputsResponse.RoundId}. Requested round: {roundId}."); } client.UniqueId = inputsResponse.UniqueId; Logger.LogInfo($"Round ({client.RoundId}), Alice ({client.UniqueId}): Registered {request.Inputs.Count()} inputs."); return(client); } catch { client?.Dispose(); throw; } }
public static async Task <AliceClient> CreateNewAsync(Network network, InputsRequest request, Uri baseUri, IPEndPoint torSocks5EndPoint = null) { AliceClient client = new AliceClient(network, baseUri, torSocks5EndPoint); try { using (HttpResponseMessage response = await client.TorClient.SendAsync(HttpMethod.Post, $"/api/v{Helpers.Constants.BackendMajorVersion}/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent())) { if (response.StatusCode != HttpStatusCode.OK) { await response.ThrowRequestExceptionFromContentAsync(); } var inputsResponse = await response.Content.ReadAsJsonAsync <InputsResponse>(); client.RoundId = inputsResponse.RoundId; client.UniqueId = inputsResponse.UniqueId; client.BlindedOutputSignature = inputsResponse.BlindedOutputSignature; Logger.LogInfo <AliceClient>($"Round ({client.RoundId}), Alice ({client.UniqueId}): Registered {request.Inputs.Count()} inputs."); return(client); } } catch { client.Dispose(); throw; } }
public static async Task <AliceClient> CreateNewAsync(InputsRequest request, Uri baseUri, IPEndPoint torSocks5EndPoint = null) { AliceClient client = new AliceClient(baseUri, torSocks5EndPoint); try { using (HttpResponseMessage response = await client.TorClient.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent())) { if (response.StatusCode != HttpStatusCode.OK) { string error = await response.Content.ReadAsJsonAsync <string>(); var errorMessage = error == null ? string.Empty : $"\n{error}"; throw new HttpRequestException($"{response.StatusCode.ToReasonString()}{errorMessage}"); } var inputsResponse = await response.Content.ReadAsJsonAsync <InputsResponse>(); client.RoundId = inputsResponse.RoundId; client.UniqueId = inputsResponse.UniqueId; client.BlindedOutputSignature = inputsResponse.BlindedOutputSignature; Logger.LogInfo <AliceClient>($"Round ({client.RoundId}), Alice ({client.UniqueId}): Registered {request.Inputs.Count()} inputs."); return(client); } } catch { client.Dispose(); throw; } }
public static async Task <AliceClient> CreateNewAsync(Network network, BitcoinAddress changeOutput, byte[] blindedData, IEnumerable <InputProofModel> inputs, Uri baseUri, IPEndPoint torSocks5EndPoint = null) { var request = new InputsRequest { BlindedOutputScriptHex = ByteHelpers.ToHex(blindedData), ChangeOutputAddress = changeOutput.ToString(), Inputs = inputs }; return(await CreateNewAsync(network, request, baseUri, torSocks5EndPoint)); }
public async Task <InputsResponse> PostInputsAsync(Script changeOutput, byte[] blindedData, params InputProofModel[] inputs) { var request = new InputsRequest { BlindedOutputScriptHex = ByteHelpers.ToHex(blindedData), ChangeOutputScript = changeOutput.ToString(), Inputs = inputs }; return(await PostInputsAsync(request)); }
public static async Task <AliceClient> CreateNewAsync( long roundId, IEnumerable <BitcoinAddress> registeredAddresses, IEnumerable <SchnorrPubKey> schnorrPubKeys, IEnumerable <Requester> requesters, Network network, InputsRequest request, Uri baseUri, EndPoint torSocks5EndPoint) { return(await CreateNewAsync(roundId, registeredAddresses, schnorrPubKeys, requesters, network, request, () => baseUri, torSocks5EndPoint).ConfigureAwait(false)); }
public static async Task <AliceClient> CreateNewAsync( long roundId, IEnumerable <BitcoinAddress> registeredAddresses, IEnumerable <SchnorrPubKey> schnorrPubKeys, IEnumerable <Requester> requesters, Network network, InputsRequest request, Uri baseUri, IPEndPoint torSocks5EndPoint = null) { AliceClient client = new AliceClient(roundId, registeredAddresses, schnorrPubKeys, requesters, network, baseUri, torSocks5EndPoint); try { // Correct it if forgot to set. if (request.RoundId != roundId) { if (request.RoundId == 0) { request.RoundId = roundId; } else { throw new NotSupportedException($"InputRequest roundId doesn't match to the provided roundId: {request.RoundId} != {roundId}."); } } using (HttpResponseMessage response = await client.TorClient.SendAsync(HttpMethod.Post, $"/api/v{Helpers.Constants.BackendMajorVersion}/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent())) { if (response.StatusCode != HttpStatusCode.OK) { await response.ThrowRequestExceptionFromContentAsync(); } var inputsResponse = await response.Content.ReadAsJsonAsync <InputsResponse>(); if (inputsResponse.RoundId != roundId) // This should never happen. If it does, that's a bug in the coordinator. { throw new NotSupportedException($"Coordinator assigned us to the wrong round: {inputsResponse.RoundId}. Requested round: {roundId}."); } client.UniqueId = inputsResponse.UniqueId; Logger.LogInfo <AliceClient>($"Round ({client.RoundId}), Alice ({client.UniqueId}): Registered {request.Inputs.Count()} inputs."); return(client); } } catch { client.Dispose(); throw; } }
public static async Task <AliceClient> CreateNewAsync(long roundId, IEnumerable <BitcoinAddress> registeredAddresses, IEnumerable <SchnorrPubKey> schnorrPubKeys, IEnumerable <Requester> requesters, Network network, BitcoinAddress changeOutput, IEnumerable <uint256> blindedOutputScriptHashes, IEnumerable <InputProofModel> inputs, Func <Uri> baseUriAction, IPEndPoint torSocks5EndPoint) { var request = new InputsRequest { RoundId = roundId, BlindedOutputScripts = blindedOutputScriptHashes, ChangeOutputAddress = changeOutput, Inputs = inputs }; return(await CreateNewAsync(roundId, registeredAddresses, schnorrPubKeys, requesters, network, request, baseUriAction, torSocks5EndPoint)); }
public async Task <InputsResponse> PostInputsAsync(InputsRequest request) { using (HttpResponseMessage response = await TorClient.SendAsync(HttpMethod.Post, "/api/v1/btc/chaumiancoinjoin/inputs/", request.ToHttpStringContent())) { if (response.StatusCode != HttpStatusCode.OK) { string error = await response.Content.ReadAsJsonAsync <string>(); if (error == null) { throw new HttpRequestException(response.StatusCode.ToReasonString()); } else { throw new HttpRequestException($"{response.StatusCode.ToReasonString()}\n{error}"); } } return(await response.Content.ReadAsJsonAsync <InputsResponse>()); } }
public async Task <InputsResponse> PostInputsAsync(InputsRequest request, CancellationToken cancel) { using (await _asyncLock.LockAsync()) { string requestJsonString = JsonConvert.SerializeObject(request); var content = new StringContent( requestJsonString, Encoding.UTF8, "application/json"); HttpResponseMessage response = await PostAsync("inputs", content, cancel); if (!response.IsSuccessStatusCode) { throw new HttpRequestException(response.StatusCode.ToString()); } string responseString = await response.Content.ReadAsStringAsync(); AssertSuccess(responseString); return(JsonConvert.DeserializeObject <InputsResponse>(responseString)); } }
public async Task <IActionResult> PostInputsAsync([FromBody] InputsRequest request) { // Validate request. if (!ModelState.IsValid || request == null || string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex) || string.IsNullOrWhiteSpace(request.ChangeOutputScript) || request.Inputs == null || request.Inputs.Count() == 0 || request.Inputs.Any(x => x.Input == null || x.Input.Hash == null || string.IsNullOrWhiteSpace(x.Proof))) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CcjRound round = Coordinator.GetCurrentInputRegisterableRound(); // Do more checks. try { if (round.ContainsBlindedOutputScriptHex(request.BlindedOutputScriptHex, out _)) { return(BadRequest("Blinded output has already been registered.")); } var changeOutput = new Script(request.ChangeOutputScript); var inputs = new HashSet <(OutPoint OutPoint, TxOut Output)>(); var alicesToRemove = new HashSet <Guid>(); foreach (InputProofModel inputProof in request.Inputs) { if (inputs.Any(x => x.OutPoint == inputProof.Input)) { return(BadRequest("Cannot register an input twice.")); } if (round.ContainsInput(inputProof.Input, out List <Alice> tr)) { alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. } if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input, out List <Alice> tnr)) { if (tr.Union(tnr).Count() > tr.Count()) { return(BadRequest("Input is already registered in another round.")); } } var bannedElem = Coordinator.UtxoReferee.BannedUtxos.SingleOrDefault(x => x.Key == inputProof.Input); if (bannedElem.Key != default) { int maxBan = (int)TimeSpan.FromDays(30).TotalMinutes; int banLeft = maxBan - (int)((DateTimeOffset.UtcNow - bannedElem.Value.timeOfBan).TotalMinutes); if (banLeft > 0) { return(BadRequest($"Input is banned from participation for {banLeft} minutes: {inputProof.Input.N}:{inputProof.Input.Hash}.")); } else { await Coordinator.UtxoReferee.UnbanAsync(bannedElem.Key); } } GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.Hash, (int)inputProof.Input.N, includeMempool : true); // Check if inputs are unspent. if (getTxOutResponse == null) { return(BadRequest("Provided input is not unspent.")); } // Check if unconfirmed. if (getTxOutResponse.Confirmations <= 0) { // If it spends a CJ then it may be acceptable to register. if (!Coordinator.ContainsCoinJoin(inputProof.Input.Hash)) { return(BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.")); } // After 24 unconfirmed cj in the mempool dont't let unconfirmed coinjoin to be registered. if (await Coordinator.IsUnconfirmedCoinJoinLimitReachedAsync()) { return(BadRequest("Provided input is from an unconfirmed coinjoin, but the maximum number of unconfirmed coinjoins is reached.")); } } // Check if immature. if (getTxOutResponse.Confirmations <= 100) { if (getTxOutResponse.IsCoinBase) { return(BadRequest("Provided input is immature.")); } } // Check if inputs are native segwit. if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { return(BadRequest("Provided input must be witness_v0_keyhash.")); } TxOut txout = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(Network); // Check if proofs are valid. bool validProof; try { validProof = address.VerifyMessage(request.BlindedOutputScriptHex, inputProof.Proof); } catch (FormatException ex) { return(BadRequest($"Provided proof is invalid: {ex.Message}")); } if (!validProof) { return(BadRequest("Provided proof is invalid.")); } inputs.Add((inputProof.Input, txout)); } // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Output.Value); Money networkFeeToPay = (inputs.Count() * round.FeePerInputs + 2 * round.FeePerOutputs); Money changeAmount = inputSum - (round.Denomination + networkFeeToPay); if (changeAmount < Money.Zero) { 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 Alice checks work. var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutputScriptHex); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. byte[] blindedData; try { blindedData = ByteHelpers.FromHex(request.BlindedOutputScriptHex); } catch { return(BadRequest("Invalid blinded output hex.")); } Logger.LogDebug <ChaumianCoinJoinController>($"Blinded data hex: {request.BlindedOutputScriptHex}"); Logger.LogDebug <ChaumianCoinJoinController>($"Blinded data array size: {blindedData.Length}"); byte[] signature = RsaKey.SignBlindedData(blindedData); // Check if phase changed since. if (round.Status != ChaumianCoinJoin.CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration) { return(base.StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again.")); } // Progress round if needed. if (round.CountAlices() >= round.AnonymitySet) { await round.RemoveAlicesIfInputsSpentAsync(); if (round.CountAlices() >= round.AnonymitySet) { await round.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation); } } var resp = new InputsResponse { UniqueId = alice.UniqueId, BlindedOutputSignature = signature, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug <ChaumianCoinJoinController>(ex); return(BadRequest(ex.Message)); } } }
public async Task <IActionResult> PostInputsAsync([FromBody, Required] InputsRequest request) { // Validate request. if (request.RoundId < 0 || !ModelState.IsValid) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CoordinatorRound round = Coordinator.TryGetRound(request.RoundId); if (round is null || round.Phase != RoundPhase.InputRegistration) { return(NotFound("No such running round in InputRegistration. Try another round.")); } // Do more checks. try { uint256[] blindedOutputs = request.BlindedOutputScripts.ToArray(); int blindedOutputCount = blindedOutputs.Length; int maxBlindedOutputCount = round.MixingLevels.Count(); if (blindedOutputCount > maxBlindedOutputCount) { return(BadRequest($"Too many blinded output was provided: {blindedOutputCount}, maximum: {maxBlindedOutputCount}.")); } if (blindedOutputs.Distinct().Count() < blindedOutputs.Length) { return(BadRequest("Duplicate blinded output found.")); } if (round.ContainsAnyBlindedOutputScript(blindedOutputs)) { return(BadRequest("Blinded output has already been registered.")); } if (request.ChangeOutputAddress.Network != Network) { // RegTest and TestNet address formats are sometimes the same. if (Network == Network.Main) { return(BadRequest($"Invalid ChangeOutputAddress Network.")); } } var uniqueInputs = new HashSet <OutPoint>(); foreach (InputProofModel inputProof in request.Inputs) { var outpoint = inputProof.Input.ToOutPoint(); if (uniqueInputs.Contains(outpoint)) { return(BadRequest("Cannot register an input twice.")); } uniqueInputs.Add(outpoint); } var alicesToRemove = new HashSet <Guid>(); var getTxOutResponses = new List <(InputProofModel inputModel, Task <GetTxOutResponse> getTxOutTask)>(); var batch = RpcClient.PrepareBatch(); foreach (InputProofModel inputProof in request.Inputs) { if (round.ContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tr)) { alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. } if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tnr)) { if (tr.Union(tnr).Count() > tr.Count) { return(BadRequest("Input is already registered in another round.")); } } OutPoint outpoint = inputProof.Input.ToOutPoint(); var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo : false); if (bannedElem != null) { return(BadRequest($"Input is banned from participation for {(int)bannedElem.BannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } var txOutResponseTask = batch.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool: true); getTxOutResponses.Add((inputProof, txOutResponseTask)); } // Perform all RPC request at once var waiting = Task.WhenAll(getTxOutResponses.Select(x => x.getTxOutTask)); await batch.SendBatchAsync(); await waiting; byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputs.Select(x => x.ToBytes())); uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte)); var inputs = new HashSet <Coin>(); foreach (var responses in getTxOutResponses) { var(inputProof, getTxOutResponseTask) = responses; var getTxOutResponse = await getTxOutResponseTask; // Check if inputs are unspent. if (getTxOutResponse is null) { return(BadRequest($"Provided input is not unspent: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } // Check if unconfirmed. if (getTxOutResponse.Confirmations <= 0) { // If it spends a CJ then it may be acceptable to register. if (!await Coordinator.ContainsUnconfirmedCoinJoinAsync(inputProof.Input.TransactionId)) { return(BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.")); } // Check if mempool would accept a fake transaction created with the registered inputs. // This will catch ascendant/descendant count and size limits for example. var result = await RpcClient.TestMempoolAcceptAsync(new[] { new Coin(inputProof.Input.ToOutPoint(), getTxOutResponse.TxOut) }); if (!result.accept) { return(BadRequest($"Provided input is from an unconfirmed coinjoin, but a limit is reached: {result.rejectReason}")); } } // Check if immature. if (getTxOutResponse.Confirmations <= 100) { if (getTxOutResponse.IsCoinBase) { return(BadRequest("Provided input is immature.")); } } // Check if inputs are native segwit. if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { return(BadRequest("Provided input must be witness_v0_keyhash.")); } TxOut txOut = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txOut.ScriptPubKey.GetDestinationAddress(Network); // Check if proofs are valid. if (!address.VerifyMessage(blindedOutputScriptsHash, inputProof.Proof)) { return(BadRequest("Provided proof is invalid.")); } inputs.Add(new Coin(inputProof.Input.ToOutPoint(), txOut)); } var acceptedBlindedOutputScripts = new List <uint256>(); // Calculate expected networkfee to pay after base denomination. int inputCount = inputs.Count; Money networkFeeToPayAfterBaseDenomination = (inputCount * round.FeePerInputs) + (2 * round.FeePerOutputs); // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Amount); Money changeAmount = (inputSum - (round.MixingLevels.GetBaseDenomination() + networkFeeToPayAfterBaseDenomination)); if (changeAmount < Money.Zero) { return(BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPayAfterBaseDenomination.ToString(false, true)} BTC. Round denomination: {round.MixingLevels.GetBaseDenomination().ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC.")); } acceptedBlindedOutputScripts.Add(blindedOutputs.First()); Money networkFeeToPay = networkFeeToPayAfterBaseDenomination; // Make sure we sign the proper number of additional blinded outputs. var moneySoFar = Money.Zero; for (int i = 1; i < blindedOutputCount; i++) { if (!round.MixingLevels.TryGetDenomination(i, out Money denomination)) { break; } Money coordinatorFee = denomination.Percentage(round.CoordinatorFeePercent * round.AnonymitySet); // It should be the number of bobs, but we must make sure they'd have money to pay all. changeAmount -= (denomination + round.FeePerOutputs + coordinatorFee); networkFeeToPay += round.FeePerOutputs; if (changeAmount < Money.Zero) { break; } acceptedBlindedOutputScripts.Add(blindedOutputs[i]); } // Make sure Alice checks work. var alice = new Alice(inputs, networkFeeToPayAfterBaseDenomination, request.ChangeOutputAddress, acceptedBlindedOutputScripts); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. var blindSignatures = new List <uint256>(); for (int i = 0; i < acceptedBlindedOutputScripts.Count; i++) { var blindedOutput = acceptedBlindedOutputScripts[i]; var signer = round.MixingLevels.GetLevel(i).Signer; uint256 blindSignature = signer.Sign(blindedOutput); blindSignatures.Add(blindSignature); } alice.BlindedOutputSignatures = blindSignatures.ToArray(); // Check if phase changed since. if (round.Status != CoordinatorRoundStatus.Running || round.Phase != RoundPhase.InputRegistration) { return(StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again.")); } // Progress round if needed. if (round.CountAlices() >= round.AnonymitySet) { await round.RemoveAlicesIfAnInputRefusedByMempoolAsync(); if (round.CountAlices() >= round.AnonymitySet) { await round.ExecuteNextPhaseAsync(RoundPhase.ConnectionConfirmation); } } var resp = new InputsResponse { UniqueId = alice.UniqueId, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug(ex); return(BadRequest(ex.Message)); } } }
public async Task <IActionResult> PostInputsAsync([FromBody] InputsRequest request) { // Validate request. if (!ModelState.IsValid || request is null || string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex) || string.IsNullOrWhiteSpace(request.ChangeOutputAddress) || request.Inputs is null || !request.Inputs.Any() || request.Inputs.Any(x => x.Input == default(TxoRef) || x.Input.TransactionId is null || string.IsNullOrWhiteSpace(x.Proof))) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CcjRound round = Coordinator.GetCurrentInputRegisterableRound(); // Do more checks. try { if (round.ContainsBlindedOutputScriptHex(request.BlindedOutputScriptHex, out _)) { return(BadRequest("Blinded output has already been registered.")); } BitcoinAddress changeOutputAddress; try { changeOutputAddress = BitcoinAddress.Create(request.ChangeOutputAddress, Network); } catch (FormatException ex) { return(BadRequest($"Invalid ChangeOutputAddress. Details: {ex.Message}")); } var inputs = new HashSet <Coin>(); var alicesToRemove = new HashSet <Guid>(); foreach (InputProofModel inputProof in request.Inputs) { if (inputs.Any(x => x.Outpoint == inputProof.Input)) { return(BadRequest("Cannot register an input twice.")); } if (round.ContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tr)) { alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. } if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tnr)) { if (tr.Union(tnr).Count() > tr.Count()) { return(BadRequest("Input is already registered in another round.")); } } OutPoint outpoint = inputProof.Input.ToOutPoint(); var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo : false); if (bannedElem != null) { return(BadRequest($"Input is banned from participation for {(int)bannedElem.Value.bannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool : true); // Check if inputs are unspent. if (getTxOutResponse is null) { return(BadRequest($"Provided input is not unspent: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } // Check if unconfirmed. if (getTxOutResponse.Confirmations <= 0) { // If it spends a CJ then it may be acceptable to register. if (!Coordinator.ContainsCoinJoin(inputProof.Input.TransactionId)) { return(BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.")); } // Check if mempool would accept a fake transaction created with the registered inputs. // This will catch ascendant/descendant count and size limits for example. var result = await RpcClient.TestMempoolAcceptAsync(new Coin(inputProof.Input.ToOutPoint(), getTxOutResponse.TxOut)); if (!result.accept) { return(BadRequest($"Provided input is from an unconfirmed coinjoin, but a limit is reached: {result.rejectReason}")); } } // Check if immature. if (getTxOutResponse.Confirmations <= 100) { if (getTxOutResponse.IsCoinBase) { return(BadRequest("Provided input is immature.")); } } // Check if inputs are native segwit. if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { return(BadRequest("Provided input must be witness_v0_keyhash.")); } TxOut txout = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(Network); // Check if proofs are valid. bool validProof; try { validProof = address.VerifyMessage(request.BlindedOutputScriptHex, inputProof.Proof); } catch (FormatException ex) { return(BadRequest($"Provided proof is invalid: {ex.Message}")); } if (!validProof) { await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, outpoint); return(BadRequest("Provided proof is invalid.")); } inputs.Add(new Coin(inputProof.Input.ToOutPoint(), txout)); } // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Amount); Money networkFeeToPay = (inputs.Count() * round.FeePerInputs) + (2 * round.FeePerOutputs); Money changeAmount = inputSum - (round.Denomination + networkFeeToPay); if (changeAmount < Money.Zero) { 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 Alice checks work. var alice = new Alice(inputs, networkFeeToPay, changeOutputAddress, request.BlindedOutputScriptHex); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. byte[] blindedData; try { blindedData = ByteHelpers.FromHex(request.BlindedOutputScriptHex); } 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) { return(base.StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again.")); } // Progress round if needed. if (round.CountAlices() >= round.AnonymitySet) { await round.RemoveAlicesIfAnInputRefusedByMempoolAsync(); if (round.CountAlices() >= round.AnonymitySet) { await round.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation); } } var resp = new InputsResponse { UniqueId = alice.UniqueId, BlindedOutputSignature = signature, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug <ChaumianCoinJoinController>(ex); return(BadRequest(ex.Message)); } } }
public async Task <IActionResult> InputsAsync([FromBody] InputsRequest request) { var roundId = Global.StateMachine.RoundId; TumblerPhase phase = TumblerPhase.InputRegistration; try { if (Global.StateMachine.Phase != TumblerPhase.InputRegistration || !Global.StateMachine.AcceptRequest) { return(new ObjectResult(new FailureResponse { Message = "Wrong phase" })); } // Check not nulls string blindedOutputString = request.BlindedOutput.Trim(); if (string.IsNullOrWhiteSpace(blindedOutputString)) { return(new BadRequestResult()); } if (string.IsNullOrWhiteSpace(request.ChangeOutput)) { return(new BadRequestResult()); } if (request.Inputs == null || request.Inputs.Count() == 0) { return(new BadRequestResult()); } // Check format (parse everyting)) if (Global.StateMachine.BlindedOutputs.Contains(blindedOutputString)) { throw new ArgumentException("Blinded output has already been registered"); } byte[] blindedOutput = HexHelpers.GetBytes(blindedOutputString); Network network = Global.Config.Network; var changeOutput = new BitcoinWitPubKeyAddress(request.ChangeOutput, expectedNetwork: network); if (request.Inputs.Count() > Global.Config.MaximumInputsPerAlices) { throw new NotSupportedException("Too many inputs provided"); } var inputs = new HashSet <(TxOut Output, OutPoint OutPoint)>(); var alicesToRemove = new HashSet <Guid>(); using (await InputRegistrationLock.LockAsync()) { foreach (InputProofModel input in request.Inputs) { var op = new OutPoint(); op.FromHex(input.Input); if (inputs.Any(x => x.OutPoint.Hash == op.Hash && x.OutPoint.N == op.N)) { throw new ArgumentException("Attempting to register an input twice is not permitted"); } foreach (var a in Global.StateMachine.Alices) { if (a.Inputs.Any(x => x.OutPoint.Hash == op.Hash && x.OutPoint.N == op.N)) { alicesToRemove.Add(a.UniqueId); // input is already registered by this alice, remove it if all the checks are completed fine } } BannedUtxo banned = Global.UtxoReferee.Utxos.FirstOrDefault(x => x.Utxo.Hash == op.Hash && x.Utxo.N == op.N); if (banned != default(BannedUtxo)) { var maxBan = (int)TimeSpan.FromDays(30).TotalMinutes; int banLeft = maxBan - (int)((DateTimeOffset.UtcNow - banned.TimeOfBan).TotalMinutes); throw new ArgumentException($"Input is banned for {banLeft} minutes"); } var getTxOutResponse = await Global.RpcClient.GetTxOutAsync(op.Hash, (int)op.N, true); // Check if inputs are unspent if (getTxOutResponse == null) { throw new ArgumentException("Provided input is not unspent"); } // Check if inputs are unconfirmed, if so check if they are part of previous CoinJoin if (getTxOutResponse.Confirmations <= 0) { if (!Global.CoinJoinStore.Transactions .Any(x => x.State >= CoinJoinTransactionState.Succeeded && x.Transaction.GetHash() == op.Hash)) { throw new ArgumentException("Provided input is not confirmed, nor spends a previous CJ transaction"); } else { // after 24 unconfirmed cj in the mempool dont't let unconfirmed coinjoin to be registered var unconfirmedCoinJoins = Global.CoinJoinStore.Transactions.Where(x => x.State == CoinJoinTransactionState.Succeeded); if (unconfirmedCoinJoins.Count() >= 24) { var toFailed = new HashSet <uint256>(); var toConfirmed = new HashSet <uint256>(); foreach (var tx in unconfirmedCoinJoins) { RPCResponse getRawTransactionResponse = (await Global.RpcClient.SendCommandAsync("getrawtransaction", tx.Transaction.GetHash().ToString(), true)); if (string.IsNullOrWhiteSpace(getRawTransactionResponse?.ResultString)) { toFailed.Add(tx.Transaction.GetHash()); } if (getRawTransactionResponse.Result.Value <int>("confirmations") > 0) { toConfirmed.Add(tx.Transaction.GetHash()); } } foreach (var tx in toFailed) { Global.CoinJoinStore.TryUpdateState(tx, CoinJoinTransactionState.Failed); } foreach (var tx in toConfirmed) { Global.CoinJoinStore.TryUpdateState(tx, CoinJoinTransactionState.Confirmed); } if (toFailed.Count + toConfirmed.Count > 0) { await Global.CoinJoinStore.ToFileAsync(Global.CoinJoinStorePath); } // if couldn't remove any unconfirmed tx then refuse registration if (Global.CoinJoinStore.Transactions.Count(x => x.State == CoinJoinTransactionState.Succeeded) >= 24) { throw new ArgumentException("Registering unconfirmed CJ transaction output is currently not allowed due to too long mempool chain"); } } } } // Check coinbase > 100 if (getTxOutResponse.Confirmations < 100) { if (getTxOutResponse.IsCoinBase) { throw new ArgumentException("Provided input is unspendable"); } } // Check if inputs are native segwit if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { throw new ArgumentException("Provided input is not witness_v0_keyhash"); } var txout = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(network); // Check if proofs are valid var validProof = address.VerifyMessage(blindedOutputString, input.Proof); if (!validProof) { throw new ArgumentException("Provided proof is invalid"); } inputs.Add((txout, op)); } // Check if inputs have enough coins Money amount = Money.Zero; foreach (Money val in inputs.Select(x => x.Output.Value)) { amount += val; } Money feeToPay = (inputs.Count() * Global.StateMachine.FeePerInputs + 2 * Global.StateMachine.FeePerOutputs); Money changeAmount = amount - (Global.StateMachine.Denomination + feeToPay); if (changeAmount < Money.Zero + new Money(548)) // 546 is dust { throw new ArgumentException("Total provided inputs must be > denomination + fee + dust"); } byte[] signature = Global.RsaKey.SignBlindedData(blindedOutput); Global.StateMachine.BlindedOutputs.Add(blindedOutputString); Guid uniqueId = Guid.NewGuid(); var alice = new Alice { UniqueId = uniqueId, ChangeOutput = changeOutput, ChangeAmount = changeAmount, State = AliceState.InputsRegistered }; alice.Inputs = new ConcurrentHashSet <(TxOut Output, OutPoint OutPoint)>(); foreach (var input in inputs) { alice.Inputs.Add(input); } AssertPhase(roundId, phase); foreach (var aliceToRemove in alicesToRemove) { if (Global.StateMachine.TryRemoveAlice(aliceToRemove)) { await Global.StateMachine.BroadcastPeerRegisteredAsync(); } } Global.StateMachine.Alices.Add(alice); await Global.StateMachine.BroadcastPeerRegisteredAsync(); if (Global.StateMachine.Alices.Count >= Global.StateMachine.AnonymitySet) { Global.StateMachine.UpdatePhase(TumblerPhase.ConnectionConfirmation); } TumblerStateMachine.EstimateInputAndOutputSizes(out int inputSizeInBytes, out int outputSizeInBytes); int estimatedTxSize = Global.StateMachine.Alices.SelectMany(x => x.Inputs).Count() * inputSizeInBytes + 2 * outputSizeInBytes; if (estimatedTxSize >= 90000) // standard transaction is < 100KB { Global.StateMachine.UpdatePhase(TumblerPhase.ConnectionConfirmation); } var ret = new ObjectResult(new InputsResponse() { UniqueId = uniqueId.ToString(), SignedBlindedOutput = HexHelpers.ToString(signature) }); return(ret); } } catch (Exception ex) { return(new ObjectResult(new FailureResponse { Message = ex.Message })); } }