private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null) { // If key list is not provided refresh the key list. if (keys == null) { keys = KeyManager.GetKeys().ToList(); } // If transaction received to any of the wallet keys: for (var i = 0; i < tx.Transaction.Outputs.Count; i++) { var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey); if (foundKey != default) { // If we already had it, just update the height. Maybe got from mempool to block or reorged. var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i); if (foundCoin != default) { // If tx height is mempool then don't, otherwise update the height. if (tx.Height == Height.MemPool) { continue; } else { foundCoin.Height = tx.Height; continue; } } foundKey.KeyState = KeyState.Used; var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, foundKey.Label, null); Coins.Add(coin); // Make sure there's always 21 clean keys generated and indexed. if (AssertCleanKeysIndexed(21, foundKey.IsInternal())) { // If it generated a new key refresh the keys: keys = KeyManager.GetKeys().ToList(); } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { foundCoin.SpenderTransactionId = tx.GetHash(); } } }
private async Task BroadcastTransactionToNetworkNodeAsync(SmartTransaction transaction, Node node) { Logger.LogInfo($"Trying to broadcast transaction with random node ({node.RemoteSocketAddress}):{transaction.GetHash()}."); if (!BitcoinStore.MempoolService.TryAddToBroadcastStore(transaction.Transaction, node.RemoteSocketEndpoint.ToString())) // So we'll reply to INV with this transaction. { Logger.LogWarning($"Transaction {transaction.GetHash()} was already present in the broadcast store."); } var invPayload = new InvPayload(transaction.Transaction); // Give 7 seconds to send the inv payload. await node.SendMessageAsync(invPayload).WithAwaitCancellationAsync(TimeSpan.FromSeconds(7)).ConfigureAwait(false); // ToDo: It's dangerous way to cancel. Implement proper cancellation to NBitcoin! if (BitcoinStore.MempoolService.TryGetFromBroadcastStore(transaction.GetHash(), out TransactionBroadcastEntry? entry)) { // Give 7 seconds for serving. var timeout = 0; while (!entry.IsBroadcasted()) { if (timeout > 7) { throw new TimeoutException("Did not serve the transaction."); } await Task.Delay(1_000).ConfigureAwait(false); timeout++; } node.DisconnectAsync("Thank you!"); Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}. Successfully broadcasted transaction: {transaction.GetHash()}."); // Give 21 seconds for propagation. timeout = 0; while (entry.GetPropagationConfirmations() < 2) { if (timeout > 21) { throw new TimeoutException("Did not serve the transaction."); } await Task.Delay(1_000).ConfigureAwait(false); timeout++; } Logger.LogInfo($"Transaction is successfully propagated: {transaction.GetHash()}."); } else { Logger.LogWarning($"Expected transaction {transaction.GetHash()} was not found in the broadcast store."); } }
private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction) { Logger.LogInfo("Broadcasting with backend..."); using (var client = new WasabiClient(Synchronizer.WasabiClient.TorClient.DestinationUriAction, Synchronizer.WasabiClient.TorClient.TorSocks5EndPoint)) { try { await client.BroadcastAsync(transaction).ConfigureAwait(false); } catch (HttpRequestException ex2) when(ex2.Message.Contains("bad-txns-inputs-missingorspent", StringComparison.InvariantCultureIgnoreCase) || ex2.Message.Contains("missing-inputs", StringComparison.InvariantCultureIgnoreCase) || ex2.Message.Contains("txn-mempool-conflict", StringComparison.InvariantCultureIgnoreCase)) { if (transaction.Transaction.Inputs.Count == 1) // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know. { OutPoint input = transaction.Transaction.Inputs.First().PrevOut; lock (WalletServicesLock) { foreach (var walletService in AliveWalletServices) { SmartCoin coin = walletService.Coins.GetByOutPoint(input); if (coin != default) { coin.SpentAccordingToBackend = true; } } } } } } BelieveTransaction(transaction); Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}."); }
private async Task BroadcastTransactionWithRpcAsync(SmartTransaction transaction) { await RpcClient.SendRawTransactionAsync(transaction.Transaction).ConfigureAwait(false); BelieveTransaction(transaction); Logger.LogInfo($"Transaction is successfully broadcasted with RPC: {transaction.GetHash()}."); }
public async Task ConfirmTransactionTestAsync() { var transactionProcessor = await CreateTransactionProcessorAsync(); var keys = transactionProcessor.KeyManager.GetKeys().ToArray(); int coinsReceived = 0; int confirmed = 0; transactionProcessor.CoinReceived += (s, e) => coinsReceived++; transactionProcessor.CoinSpent += (s, e) => throw new InvalidOperationException("We are not spending the coin."); transactionProcessor.SpenderConfirmed += (s, e) => confirmed++; // A confirmed segwit transaction for us var tx = CreateCreditingTransaction(keys[0].PubKey.WitHash.ScriptPubKey, Money.Coins(1.0m)); transactionProcessor.Process(tx); Assert.Equal(1, coinsReceived); var coin = Assert.Single(transactionProcessor.Coins); Assert.False(coin.Confirmed); var tx2 = new SmartTransaction(tx.Transaction.Clone(), new Height(54321)); Assert.Equal(tx.GetHash(), tx2.GetHash()); transactionProcessor.Process(tx2); Assert.Equal(1, coinsReceived); Assert.True(coin.Confirmed); Assert.Equal(0, confirmed); }
public bool TryUpdate(SmartTransaction tx) { var hash = tx.GetHash(); lock (Lock) { // Do Contains first, because it's fast. if (ConfirmedStore.TryUpdate(tx)) { return(true); } else if (tx.Confirmed && MempoolStore.TryRemove(hash, out var originalTx)) { originalTx.TryUpdate(tx); ConfirmedStore.TryAddOrUpdate(originalTx); return(true); } else if (MempoolStore.TryUpdate(tx)) { return(true); } } return(false); }
private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction) { Logger.LogInfo("Broadcasting with backend..."); using (TorHttpClient torHttpClient = Synchronizer.HttpClientFactory.NewBackendTorHttpClient(isolateStream: true)) { var client = new WasabiClient(torHttpClient); try { await client.BroadcastAsync(transaction).ConfigureAwait(false); } catch (HttpRequestException ex2) when(RpcErrorTools.IsSpentError(ex2.Message)) { if (transaction.Transaction.Inputs.Count == 1) // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know. { OutPoint input = transaction.Transaction.Inputs.First().PrevOut; foreach (var coin in WalletManager.CoinsByOutPoint(input)) { coin.SpentAccordingToBackend = true; } } throw; } } BelieveTransaction(transaction); Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}."); }
private void AnalyzeCoinjoin(SmartTransaction tx, int newInputAnonset, ISet <HdPubKey> distinctWalletInputPubKeys) { var indistinguishableWalletOutputs = tx .WalletOutputs.GroupBy(x => x.Amount) .ToDictionary(x => x.Key, y => y.Count()); var anonsets = tx.Transaction.GetAnonymitySets(tx.WalletOutputs.Select(x => x.Index)); foreach (var newCoin in tx.WalletOutputs.ToArray()) { // Begin estimating the anonymity set size based on the number of // equivalent outputs that the i-th output has in in the transaction. int anonset = anonsets[newCoin.Index]; // Picking randomly an output would make our anonset: total/ours. anonset /= indistinguishableWalletOutputs[newCoin.Amount]; // Account for the inherited anonymity set size from the inputs in the // anonymity set size estimate. // The anonymity set size estimated for the input cluster is corrected // by -1 to avoid double counting ourselves in the anonset. // Stated differently, the right value to use for the calculation is not the // anonymity set size, but the subset of only the other participants, since // every output must belong to a member of the set. anonset += newInputAnonset - 1; HdPubKey hdPubKey = newCoin.HdPubKey; uint256 txid = tx.GetHash(); if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet) { // If the new coin's HD pubkey haven't been used yet // then its anonset haven't been set yet. // In that case the acquired anonset does not have to be intersected with the default anonset, // so this coin gets the aquired anonset. hdPubKey.SetAnonymitySet(anonset, txid); } else if (distinctWalletInputPubKeys.Contains(hdPubKey)) { // If it's a reuse of an input's pubkey, then intersection punishment is senseless. hdPubKey.SetAnonymitySet(newInputAnonset, txid); } else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey)) { // If it's a reuse of another output' pubkey, then intersection punishment can only go as low as the inherited anonset. hdPubKey.SetAnonymitySet(Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1)), txid); } else if (hdPubKey.AnonymitySetReasons.Contains(txid)) { // If we already processed this transaction for this script // then we'll go with normal processing, it's not an address reuse, // it's just we're processing the transaction twice. hdPubKey.SetAnonymitySet(anonset, txid); } else { // It's address reuse. hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid); } } }
private void OnNext() { Navigate().Clear(); var walletViewModel = UiServices.WalletManager.GetWalletViewModel(_wallet); walletViewModel.History.SelectTransaction(_finalTransaction.GetHash()); }
private static void AnalyzeReceive(SmartTransaction tx) { // No matter how much anonymity a user would had gained in a tx, if the money comes from outside, then make anonset 1. foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey)) { key.SetAnonymitySet(1, tx.GetHash()); } }
public TransactionBroadcastEntry(SmartTransaction transaction, string nodeRemoteSocketEndpoint) { Lock = new object(); Transaction = transaction; TransactionId = Transaction.GetHash(); Broadcasted = false; PropagationConfirmations = 0; NodeRemoteSocketEndpoint = nodeRemoteSocketEndpoint; }
public async Task SendTransactionAsync(SmartTransaction transaction) { try { // Broadcast to a random node. // Wait until it arrives to at least two other nodes. // If something's wrong, fall back broadcasting with rpc, then backend. if (Network == Network.RegTest) { throw new InvalidOperationException($"Transaction broadcasting to nodes does not work in {Network.RegTest}."); } Node node = Nodes.ConnectedNodes.RandomElement(); while (node == default(Node) || !node.IsConnected || Nodes.ConnectedNodes.Count < 5) { // As long as we are connected to at least 4 nodes, we can always try again. // 3 should be enough, but make it 5 so 2 nodes could disconnect the meantime. if (Nodes.ConnectedNodes.Count < 5) { throw new InvalidOperationException("We are not connected to enough nodes."); } await Task.Delay(100).ConfigureAwait(false); node = Nodes.ConnectedNodes.RandomElement(); } await BroadcastTransactionToNetworkNodeAsync(transaction, node).ConfigureAwait(false); } catch (Exception ex) { Logger.LogInfo($"Random node could not broadcast transaction. Reason: {ex.Message}."); Logger.LogDebug(ex); if (RpcClient != null) { try { await BroadcastTransactionWithRpcAsync(transaction).ConfigureAwait(false); } catch (Exception ex2) { Logger.LogInfo($"RPC could not broadcast transaction. Reason: {ex2.Message}."); Logger.LogDebug(ex2); await BroadcastTransactionToBackendAsync(transaction).ConfigureAwait(false); } } else { await BroadcastTransactionToBackendAsync(transaction).ConfigureAwait(false); } } finally { BitcoinStore.MempoolService.TryRemoveFromBroadcastStore(transaction.GetHash(), out _); // Remove it just to be sure. Probably has been removed previously. } }
private bool TryUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryGetValue(hash, out var found)) { return(found.TryUpdate(tx)); } return(false); }
private static void AnalyzeNormalSpend(SmartTransaction tx) { // If all our inputs are ours and there's more than one output that isn't, // then we can assume that the persons the money was sent to learnt our inputs. // AND if there're outputs that go to someone else, // then we can assume that the people learnt our change outputs, // or at the very least assume that all the changes in the tx is ours. // For example even if the assumed change output is a payment to someone, a blockchain analyzer // probably would just assume it's ours and go on with its life. foreach (var key in tx.WalletInputs.Concat(tx.WalletOutputs).Select(x => x.HdPubKey)) { key.SetAnonymitySet(1, tx.GetHash()); } }
private void AnalyzeSelfSpend(SmartTransaction tx, int newInputAnonset) { foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey)) { if (key.AnonymitySet == HdPubKey.DefaultHighAnonymitySet) { key.SetAnonymitySet(newInputAnonset, tx.GetHash()); } else { key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash()); } } }
public bool TryAddToBroadcastStore(SmartTransaction transaction, string nodeRemoteSocketEndpoint) { lock (BroadcastStoreLock) { if (BroadcastStore.Any(x => x.TransactionId == transaction.GetHash())) { return(false); } else { var entry = new TransactionBroadcastEntry(transaction, nodeRemoteSocketEndpoint); BroadcastStore.Add(entry); return(true); } } }
/// <summary> /// Adjusts the anonset of the inputs to the newly calculated output anonsets. /// </summary> private static void AdjustWalletInputs(SmartTransaction tx, HashSet <HdPubKey> distinctWalletInputPubKeys, int newInputAnonset) { // Sanity check. if (!tx.WalletOutputs.Any()) { return; } var smallestOutputAnonset = tx.WalletOutputs.Min(x => x.HdPubKey.AnonymitySet); if (smallestOutputAnonset < newInputAnonset) { foreach (var key in distinctWalletInputPubKeys) { key.SetAnonymitySet(smallestOutputAnonset, tx.GetHash()); } } }
public SmartCoin(SmartTransaction transaction, uint outputIndex, HdPubKey pubKey) { Transaction = transaction; Index = outputIndex; _transactionId = new Lazy <uint256>(() => Transaction.GetHash(), true); _outPoint = new Lazy <OutPoint>(() => new OutPoint(TransactionId, Index), true); _txOut = new Lazy <TxOut>(() => Transaction.Transaction.Outputs[Index], true); _coin = new Lazy <Coin>(() => new Coin(OutPoint, TxOut), true); _hashCode = new Lazy <int>(() => OutPoint.GetHashCode(), true); Height = transaction.Height; HdPubKey = pubKey; Transaction.WalletOutputs.Add(this); }
/// <param name="newInputAnonset">The new anonymity set of the inputs.</param> private void AnalyzeWalletInputs(SmartTransaction tx, out HashSet <HdPubKey> distinctWalletInputPubKeys, out int newInputAnonset) { // We want to weaken the punishment if the input merge happens in coinjoins. // Our strategy would be is to set the coefficient in proportion to our own inputs compared to the total inputs of the transaction. // However the accuracy can be increased if we consider every input with the same pubkey as a single input entity. // This we can only do for our own inputs as we don't know the pubkeys - nor the scripts - of other inputs. // Another way to think about this is: reusing pubkey on the input side is good, the punishment happened already. distinctWalletInputPubKeys = tx.WalletInputs.Select(x => x.HdPubKey).ToHashSet(); var distinctWalletInputPubKeyCount = distinctWalletInputPubKeys.Count; var pubKeyReuseCount = tx.WalletInputs.Count - distinctWalletInputPubKeyCount; double coefficient = (double)distinctWalletInputPubKeyCount / (tx.Transaction.Inputs.Count - pubKeyReuseCount); newInputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet), coefficient); foreach (var key in distinctWalletInputPubKeys) { key.SetAnonymitySet(newInputAnonset, tx.GetHash()); } }
private (bool isAdded, bool isUpdated) TryAddOrUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryAdd(hash, tx)) { return(true, false); } else { if (Transactions[hash].TryUpdate(tx)) { return(false, true); } else { return(false, false); } } }
private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction) { Logger.LogInfo("Broadcasting with backend..."); IHttpClient httpClient = HttpClientFactory.NewHttpClientWithCircuitPerRequest(); WasabiClient client = new(httpClient); try { await client.BroadcastAsync(transaction).ConfigureAwait(false); } catch (HttpRequestException ex2) when(RpcErrorTools.IsSpentError(ex2.Message)) { if (transaction.Transaction.Inputs.Count == 1) // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know. { OutPoint input = transaction.Transaction.Inputs.First().PrevOut; foreach (var coin in WalletManager.CoinsByOutPoint(input)) { coin.SpentAccordingToBackend = true; } } // Exception message is in form: 'message:::tx1:::tx2:::etc.' where txs are encoded in HEX. IEnumerable <SmartTransaction> txs = ex2.Message.Split(":::", StringSplitOptions.RemoveEmptyEntries) .Skip(1) // Skip the exception message. .Select(x => new SmartTransaction(Transaction.Parse(x, Network), Height.Mempool)); foreach (var tx in txs) { WalletManager.Process(tx); } throw; } BelieveTransaction(transaction); Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}."); }
private void AddOrUpdateNoLock(SmartTransaction tx) { var hash = tx.GetHash(); if (tx.Confirmed) { if (MempoolStore.TryRemove(hash, out var found)) { found.TryUpdate(tx); ConfirmedStore.TryAddOrUpdate(found); } else { ConfirmedStore.TryAddOrUpdate(tx); } } else { if (!ConfirmedStore.TryUpdate(tx)) { MempoolStore.TryAddOrUpdate(tx); } } }
private ProcessedResult ProcessNoLock(SmartTransaction tx) { var result = new ProcessedResult(tx); // We do not care about non-witness transactions for other than mempool cleanup. if (tx.Transaction.PossiblyP2WPKHInvolved()) { uint256 txId = tx.GetHash(); // Performance ToDo: txids could be cached in a hashset here by the AllCoinsView and then the contains would be fast. if (!tx.Transaction.IsCoinBase && !Coins.AsAllCoinsView().CreatedBy(txId).Any()) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (var txin in tx.Transaction.Inputs) { if (Coins.TryGetSpenderSmartCoinsByOutPoint(txin.PrevOut, out var coins)) { doubleSpends.AddRange(coins); } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spending transaction but a replacement transaction. var isReplacementTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed); if (isReplacementTx) { // Undo the replaced transaction by removing the coins it created (if other coin // spends it, remove that too and so on) and restoring those that it replaced. // After undoing the replaced transaction it will process the replacement transaction. var replacedTxId = doubleSpends.First().TransactionId; var(replaced, restored) = Coins.Undo(replacedTxId); result.ReplacedCoins.AddRange(replaced); result.RestoredCoins.AddRange(restored); foreach (var replacedTransactionId in replaced.Select(coin => coin.TransactionId)) { TransactionStore.MempoolStore.TryRemove(replacedTransactionId, out _); } tx.SetReplacement(); } else { return(result); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.Remove(doubleSpentCoin); } result.SuccessfullyDoubleSpentCoins.AddRange(doubleSpends); var unconfirmedDoubleSpentTxId = doubleSpends.First().TransactionId; TransactionStore.MempoolStore.TryRemove(unconfirmedDoubleSpentTxId, out _); } } } List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey is { })
public bool Process(SmartTransaction tx) { if (!tx.Transaction.PossiblyP2WPKHInvolved()) { return(false); // We do not care about non-witness transactions for other than mempool cleanup. } uint256 txId = tx.GetHash(); var walletRelevant = false; if (tx.Confirmed) { foreach (var coin in Coins.Where(x => x.TransactionId == txId)) { coin.Height = tx.Height; walletRelevant = true; // relevant } } if (!tx.Transaction.IsCoinBase) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (SmartCoin coin in Coins) { var spent = false; foreach (TxoRef spentOutput in coin.SpentOutputs) { foreach (TxIn txIn in tx.Transaction.Inputs) { if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N) // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null. { doubleSpends.Add(coin); spent = true; walletRelevant = true; break; } } if (spent) { break; } } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spanding transaction but a replacement transaction. if (doubleSpends.Any(x => x.IsReplaceable)) { // remove double spent coins (if other coin spends it, remove that too and so on) // will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends.Where(x => !x.Confirmed)) { Coins.TryRemove(doubleSpentCoin); } tx.SetReplacement(); walletRelevant = true; } else { return(false); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.TryRemove(doubleSpentCoin); } walletRelevant = true; } } } var isLikelyCoinJoinOutput = false; bool hasEqualOutputs = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default; if (hasEqualOutputs) { var receiveKeys = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript)); bool allReceivedInternal = receiveKeys.All(x => x.IsInternal); if (allReceivedInternal) { // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs. Money spentAmount = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).Sum(x => x.Amount); Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value); bool receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m)); if (receivedAlmostAsMuchAsSpent) { isLikelyCoinJoinOutput = true; } } } List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey != default) { walletRelevant = true; if (output.Value <= DustThreshold) { continue; } foundKey.SetKeyState(KeyState.Used, KeyManager); spentOwnCoins ??= Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList(); var anonset = tx.Transaction.GetAnonymitySet(i); if (spentOwnCoins.Count != 0) { anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1; // Minus 1, because do not count own. } SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. // If we did not have it. if (Coins.TryAdd(newCoin)) { CoinReceived?.Invoke(this, newCoin); // Make sure there's always 21 clean keys generated and indexed. KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal); if (foundKey.IsInternal) { // Make sure there's always 14 internal locked keys generated and indexed. KeyManager.AssertLockedInternalKeysIndexed(14); } } else // If we had this coin already. { if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had. { SmartCoin oldCoin = Coins.FirstOrDefault(x => x.TransactionId == txId && x.Index == i); if (oldCoin != null) // Just to be sure, it is a concurrent collection. { oldCoin.Height = newCoin.Height; } } } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { walletRelevant = true; var alreadyKnown = foundCoin.SpenderTransactionId == txId; foundCoin.SpenderTransactionId = txId; if (!alreadyKnown) { CoinSpent?.Invoke(this, foundCoin); } if (tx.Confirmed) { SpenderConfirmed?.Invoke(this, foundCoin); } } } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } return(walletRelevant); }
private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null) { //iterate tx // if already have the coin // if NOT mempool // update height //if double spend // if mempool // if all double spent coins are mempool and RBF // remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys // else // return // else // new confirmation always enjoys priority // remove double spent coins recursively(if other coin spends it, remove that too and so on)// will add later if they came to our keys //iterate tx // if came to our keys // add coin // If key list is not provided refresh the key list. if (keys == null) { keys = KeyManager.GetKeys().ToList(); } for (var i = 0; i < tx.Transaction.Outputs.Count; i++) { // If we already had it, just update the height. Maybe got from mempool to block or reorged. var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i); if (foundCoin != default) { // If tx height is mempool then don't, otherwise update the height. if (tx.Height != Height.MemPool) { foundCoin.Height = tx.Height; } } } // If double spend: IEnumerable <SmartCoin> doubleSpends = Coins.Where(x => tx.Transaction.Inputs.Any(y => x.SpentOutputs.Select(z => z.ToOutPoint()).Contains(y.PrevOut))); if (doubleSpends.Count() > 0) { if (tx.Height == Height.MemPool) { // if all double spent coins are mempool and RBF if (doubleSpends.All(x => x.Height == Height.MemPool && x.RBF)) { // remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys foreach (var doubleSpentCoin in doubleSpends) { RemoveCoinRecursively(doubleSpentCoin); } } else { return; } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (var doubleSpentCoin in doubleSpends) { RemoveCoinRecursively(doubleSpentCoin); } } } for (var i = 0; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey); if (foundKey != default) { foundKey.KeyState = KeyState.Used; var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.Transaction.RBF, foundKey.Label, spenderTransactionId: null, locked: false); // Don't inherit locked status from key, that's different. Coins.Add(coin); // Make sure there's always 21 clean keys generated and indexed. if (AssertCleanKeysIndexed(21, foundKey.IsInternal())) { // If it generated a new key refresh the keys: keys = KeyManager.GetKeys().ToList(); } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { foundCoin.SpenderTransactionId = tx.GetHash(); } } }
private void AnalyzeCoinjoinWalletOutputs(SmartTransaction tx, int startingOutputAnonset, ISet <HdPubKey> distinctWalletInputPubKeys) { var indistinguishableWalletOutputs = tx .WalletOutputs.GroupBy(x => x.Amount) .ToDictionary(x => x.Key, y => y.Count()); var indistinguishableOutputs = tx.Transaction.Outputs .GroupBy(x => x.ScriptPubKey) .ToDictionary(x => x.Key, y => y.Sum(z => z.Value)) .GroupBy(x => x.Value) .ToDictionary(x => x.Key, y => y.Count()); var inputCount = tx.Transaction.Inputs.Count; var ownInputCount = tx.WalletInputs.Count; foreach (var newCoin in tx.WalletOutputs) { var output = newCoin.TxOut; var equalOutputCount = indistinguishableOutputs[output.Value]; var ownEqualOutputCount = indistinguishableWalletOutputs[output.Value]; // Anonset gain cannot be larger than others' input count. var anonset = Math.Min(equalOutputCount - ownEqualOutputCount, inputCount - ownInputCount); // Picking randomly an output would make our anonset: total/ours. anonset /= ownEqualOutputCount; // Account for the inherited anonymity set size from the inputs in the // anonymity set size estimate. anonset += startingOutputAnonset; HdPubKey hdPubKey = newCoin.HdPubKey; uint256 txid = tx.GetHash(); if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet) { // If the new coin's HD pubkey haven't been used yet // then its anonset haven't been set yet. // In that case the acquired anonset does not have to be intersected with the default anonset, // so this coin gets the acquired anonset. hdPubKey.SetAnonymitySet(anonset, txid); } else if (distinctWalletInputPubKeys.Contains(hdPubKey)) { // If it's a reuse of an input's pubkey, then intersection punishment is senseless. hdPubKey.SetAnonymitySet(startingOutputAnonset, txid); } else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey)) { // If it's a reuse of another output's pubkey, then intersection punishment can only go as low as the inherited anonset. hdPubKey.SetAnonymitySet(Math.Max(startingOutputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet })), txid); } else if (hdPubKey.OutputAnonSetReasons.Contains(txid)) { // If we already processed this transaction for this script // then we'll go with normal processing. // It may be a duplicated processing or new information arrived (like other wallet loaded) // If there are more anonsets already // then it's address reuse that we have already punished so leave it alone. if (hdPubKey.OutputAnonSetReasons.Count == 1) { hdPubKey.SetAnonymitySet(anonset, txid); } } else { // It's address reuse. hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }), txid); } } }
private ProcessedResult ProcessNoLock(SmartTransaction tx) { var result = new ProcessedResult(tx); // We do not care about non-witness transactions for other than mempool cleanup. if (tx.Transaction.PossiblyP2WPKHInvolved()) { uint256 txId = tx.GetHash(); // Performance ToDo: txids could be cached in a hashset here by the AllCoinsView and then the contains would be fast. if (!tx.Transaction.IsCoinBase && !Coins.AsAllCoinsView().CreatedBy(txId).Any()) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (var txin in tx.Transaction.Inputs) { if (Coins.TryGetSpenderSmartCoinsByOutPoint(txin.PrevOut, out var coins)) { doubleSpends.AddRange(coins); } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spending transaction but a replacement transaction. var isReplacemenetTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed); if (isReplacemenetTx) { // Undo the replaced transaction by removing the coins it created (if other coin // spends it, remove that too and so on) and restoring those that it replaced. // After undoing the replaced transaction it will process the replacement transaction. var replacedTxId = doubleSpends.First().TransactionId; var(replaced, restored) = Coins.Undo(replacedTxId); result.ReplacedCoins.AddRange(replaced); result.RestoredCoins.AddRange(restored); foreach (var replacedTransactionId in replaced.Select(coin => coin.TransactionId)) { TransactionStore.MempoolStore.TryRemove(replacedTransactionId, out _); } tx.SetReplacement(); } else { return(result); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.Remove(doubleSpentCoin); } result.SuccessfullyDoubleSpentCoins.AddRange(doubleSpends); var unconfirmedDoubleSpentTxId = doubleSpends.First().TransactionId; TransactionStore.MempoolStore.TryRemove(unconfirmedDoubleSpentTxId, out _); } } } List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey != default) { if (output.Value <= DustThreshold) { result.ReceivedDusts.Add(output); continue; } foundKey.SetKeyState(KeyState.Used, KeyManager); spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList(); var anonset = tx.Transaction.GetAnonymitySet(i); if (spentOwnCoins.Count != 0) { anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1; // Minus 1, because do not count own. } SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. result.ReceivedCoins.Add(newCoin); // If we did not have it. if (Coins.TryAdd(newCoin)) { result.NewlyReceivedCoins.Add(newCoin); // Make sure there's always 21 clean keys generated and indexed. KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal); if (foundKey.IsInternal) { // Make sure there's always 14 internal locked keys generated and indexed. KeyManager.AssertLockedInternalKeysIndexed(14); } } else // If we had this coin already. { if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had. { SmartCoin oldCoin = Coins.AsAllCoinsView().GetByOutPoint(new OutPoint(txId, i)); if (oldCoin is { }) // Just to be sure, it is a concurrent collection.
private void MemPoolService_TransactionReceived(object sender, SmartTransaction e) { Interlocked.Increment(ref _mempoolTransactionCount); Logger.LogDebug <P2pTests>($"Mempool transaction received: {e.GetHash()}."); }
private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null) { if (tx.Height != Height.MemPool && tx.Height != Height.Unknown) { MemPool.TransactionHashes.TryRemove(tx.GetHash()); } //iterate tx // if already have the coin // if NOT mempool // update height //if double spend // if mempool // if all double spent coins are mempool and RBF // remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys // else // return // else // new confirmation always enjoys priority // remove double spent coins recursively(if other coin spends it, remove that too and so on)// will add later if they came to our keys //iterate tx // if came to our keys // add coin // If key list is not provided refresh the key list. if (keys == null) { keys = KeyManager.GetKeys().ToList(); } for (var i = 0; i < tx.Transaction.Outputs.Count; i++) { // If we already had it, just update the height. Maybe got from mempool to block or reorged. var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i); if (foundCoin != default) { // If tx height is mempool then don't, otherwise update the height. if (tx.Height != Height.MemPool) { foundCoin.Height = tx.Height; } } } // If double spend: List <SmartCoin> doubleSpends = Coins .Where(x => tx.Transaction.Inputs.Any(y => x.SpentOutputs.Select(z => z.ToOutPoint()).Contains(y.PrevOut))) .ToList(); if (doubleSpends.Any()) { if (tx.Height == Height.MemPool) { // if all double spent coins are mempool and RBF if (doubleSpends.All(x => x.Height == Height.MemPool && x.RBF)) { // remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys foreach (var doubleSpentCoin in doubleSpends) { RemoveCoinRecursively(doubleSpentCoin); } } else { return; } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (var doubleSpentCoin in doubleSpends) { RemoveCoinRecursively(doubleSpentCoin); } } } for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey); if (foundKey != default) { foundKey.SetKeyState(KeyState.Used, KeyManager); List <SmartCoin> spentOwnCoins = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList(); var mixin = tx.Transaction.GetMixin(i); if (spentOwnCoins.Count != 0) { mixin += spentOwnCoins.Min(x => x.Mixin); } var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.Transaction.RBF, mixin, foundKey.Label, spenderTransactionId: null, locked: false); // Don't inherit locked status from key, that's different. Coins.TryAdd(coin); if (coin.Unspent && coin.Label == "ZeroLink Change" && ChaumianClient.OnePiece != null) { Task.Run(async() => { try { await ChaumianClient.QueueCoinsToMixAsync(ChaumianClient.OnePiece, coin); } catch (Exception ex) { Logger.LogError <WalletService>(ex); } }); } // Make sure there's always 21 clean keys generated and indexed. if (AssertCleanKeysIndexed(21, foundKey.IsInternal())) { // If it generated a new key refresh the keys: keys = KeyManager.GetKeys().ToList(); } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { foundCoin.SpenderTransactionId = tx.GetHash(); CoinSpentOrSpenderConfirmed?.Invoke(this, foundCoin); } } }