public IActionResult CoinJoin([FromBody] CoinJoinRequest request) { var roundId = Global.StateMachine.RoundId; TumblerPhase phase = TumblerPhase.Signing; try { if (Global.StateMachine.Phase != TumblerPhase.Signing || !Global.StateMachine.AcceptRequest) { return(new ObjectResult(new FailureResponse { Message = "Wrong phase" })); } if (string.IsNullOrWhiteSpace(request.UniqueId)) { return(new BadRequestResult()); } Global.StateMachine.FindAlice(request.UniqueId, throwException: true); AssertPhase(roundId, phase); return(new ObjectResult(new CoinJoinResponse { Transaction = Global.StateMachine.UnsignedCoinJoinHex })); } catch (Exception ex) { return(new ObjectResult(new FailureResponse { Message = ex.Message })); } }
private static void AssertPhase(int roundId, TumblerPhase phase) { if (Global.StateMachine.Phase != phase || Global.StateMachine.RoundId != roundId) { throw new InvalidOperationException("Phase timed out"); } }
public TumblerStateMachine() { Phase = TumblerPhase.InputRegistration; RoundId = 0; AcceptRequest = false; FallBackRound = false; CoinJoin = null; InputRegistrationStopwatch = new Stopwatch(); }
public void UpdatePhase(TumblerPhase phase) { if (phase == Phase) { return; } AcceptRequest = false; Phase = phase; _ctsPhaseCancel.Cancel(); _ctsPhaseCancel = new CancellationTokenSource(); }
public IActionResult ConnectionConfirmation([FromBody] ConnectionConfirmationRequest request) { var roundId = Global.StateMachine.RoundId; TumblerPhase phase = TumblerPhase.ConnectionConfirmation; try { if (Global.StateMachine.Phase != TumblerPhase.ConnectionConfirmation || !Global.StateMachine.AcceptRequest) { return(new ObjectResult(new FailureResponse { Message = "Wrong phase" })); } if (string.IsNullOrWhiteSpace(request.UniqueId)) { return(new BadRequestResult()); } Alice alice = Global.StateMachine.FindAlice(request.UniqueId, throwException: true); if (alice.State == AliceState.ConnectionConfirmed) { return(new ObjectResult(new ConnectionConfirmationResponse { RoundHash = Global.StateMachine.RoundHash })); } AssertPhase(roundId, phase); alice.State = AliceState.ConnectionConfirmed; try { return(new ObjectResult(new ConnectionConfirmationResponse { RoundHash = Global.StateMachine.RoundHash })); } finally { if (Global.StateMachine.Alices.All(x => x.State == AliceState.ConnectionConfirmed)) { Global.StateMachine.UpdatePhase(TumblerPhase.OutputRegistration); } } } catch (Exception ex) { return(new ObjectResult(new FailureResponse { Message = 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 })); } }
public IActionResult Output([FromBody] OutputRequest request) { var roundId = Global.StateMachine.RoundId; TumblerPhase phase = TumblerPhase.OutputRegistration; try { if (Global.StateMachine.Phase != TumblerPhase.OutputRegistration || !Global.StateMachine.AcceptRequest) { return(new ObjectResult(new FailureResponse { Message = "Wrong phase" })); } if (string.IsNullOrWhiteSpace(request.Output)) { return(new BadRequestResult()); } if (string.IsNullOrWhiteSpace(request.Signature)) { return(new BadRequestResult()); } if (string.IsNullOrWhiteSpace(request.RoundHash)) { return(new BadRequestResult()); } if (request.RoundHash != Global.StateMachine.RoundHash) { throw new ArgumentException("Wrong round hash provided"); } var output = new BitcoinWitPubKeyAddress(request.Output, expectedNetwork: Global.Config.Network); // if not already registered if (Global.StateMachine.Bobs.Any(x => x.Output == output)) { return(new ObjectResult(new SuccessResponse())); } if (Global.RsaKey.PubKey.Verify(HexHelpers.GetBytes(request.Signature), Encoding.UTF8.GetBytes(request.Output))) { try { AssertPhase(roundId, phase); Global.StateMachine.Bobs.Add(new Bob { Output = output }); return(new ObjectResult(new SuccessResponse())); } finally { if (Global.StateMachine.Alices.Count == Global.StateMachine.Bobs.Count) { Global.StateMachine.UpdatePhase(TumblerPhase.Signing); } } } else { throw new ArgumentException("Bad output"); } } catch (Exception ex) { return(new ObjectResult(new FailureResponse { Message = ex.Message })); } }