Fix RBF undoing process
MODIFY
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
@@ -231,6 +231,41 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
Assert.True(coin.IsReplaceable);
}
+ [Fact]
+ public async Task HandlesRbfWithLessCoinsAsync()
+ {
+ // Replaces a previous RBF transaction by a new one that contains one more input (higher fee)
+ var transactionProcessor = await CreateTransactionProcessorAsync();
+ Script NewScript(string label) => transactionProcessor.NewKey(label).P2wpkhScript;
+
+ // A confirmed segwit transaction for us
+ var tx = CreateCreditingTransaction( NewScript("A"), Money.Coins(1.0m), height: 54321);
+ transactionProcessor.Process(tx);
+
+ // Another confirmed segwit transaction for us
+ tx = CreateCreditingTransaction(NewScript("B"), Money.Coins(1.0m), height: 54321);
+ transactionProcessor.Process(tx);
+
+ var createdCoins = transactionProcessor.Coins.Select(x=> x.GetCoin()).ToArray(); // 2 coins of 1.0 btc each
+
+ // Spend the received coins (replaceable)
+ var destinationScript = NewScript("myself");;
+ var changeScript = NewScript("Change myself");
+ tx = CreateSpendingTransaction(createdCoins, destinationScript, changeScript); // spends 1.2btc
+ tx.Transaction.Inputs[0].Sequence = Sequence.OptInRBF;
+ var relevant = transactionProcessor.Process(tx);
+ Assert.True(relevant);
+
+ // replace previous tx with another one spending only one coin
+ destinationScript = new Key().PubKey.WitHash.ScriptPubKey; // spend to someone else
+ tx = CreateSpendingTransaction(createdCoins.Take(1), destinationScript, changeScript); // spends 1.2btc
+ relevant = transactionProcessor.Process(tx);
+
+ Assert.True(relevant);
+ Assert.Equal(2, transactionProcessor.Coins.Count());
+ //Assert.True(coin.IsReplaceable);
+ }
+
[Fact]
public async Task ReceiveTransactionForWalletAsync()
{

MODIFY
WalletWasabi/Coins/CoinsRegistry.cs
WalletWasabi/Coins/CoinsRegistry.cs
@@ -15,6 +15,7 @@ namespace WalletWasabi.Coins
private bool InvalidateSnapshot { get; set; }
private object Lock { get; set; }
private HashSet<SmartCoin> SpentCoins { get; }
+ private HashSet<SmartCoin> LatestSpentCoinsSnapshot { get; set; }
private Dictionary<Script, Cluster> ClustersByScriptPubKey { get; }
private int PrivacyLevelThreshold { get; }
@@ -23,6 +24,7 @@ namespace WalletWasabi.Coins
Coins = new HashSet<SmartCoin>();
SpentCoins = new HashSet<SmartCoin>();
LatestCoinsSnapshot = new HashSet<SmartCoin>();
+ LatestSpentCoinsSnapshot = new HashSet<SmartCoin>();
InvalidateSnapshot = false;
ClustersByScriptPubKey = new Dictionary<Script, Cluster>();
PrivacyLevelThreshold = privacyLevelThreshold;
@@ -34,11 +36,23 @@ namespace WalletWasabi.Coins
if (InvalidateSnapshot)
{
LatestCoinsSnapshot = Coins.ToHashSet(); // Creates a clone
+ LatestSpentCoinsSnapshot = SpentCoins.ToHashSet(); // Creates a clone
InvalidateSnapshot = false;
}
return new CoinsView(LatestCoinsSnapshot);
}
+ private CoinsView AsSpentCoinsViewNoLock()
+ {
+ if (InvalidateSnapshot)
+ {
+ LatestCoinsSnapshot = Coins.ToHashSet(); // Creates a clone
+ LatestSpentCoinsSnapshot = SpentCoins.ToHashSet(); // Creates a clone
+ InvalidateSnapshot = false;
+ }
+ return new CoinsView(LatestSpentCoinsSnapshot);
+ }
+
private CoinsView AsCoinsView()
{
lock (Lock)
@@ -47,6 +61,14 @@ namespace WalletWasabi.Coins
}
}
+ private CoinsView AsSpentCoinsView()
+ {
+ lock (Lock)
+ {
+ return AsSpentCoinsViewNoLock();
+ }
+ }
+
public bool IsEmpty => !AsCoinsView().Any();
public SmartCoin GetByOutPoint(OutPoint outpoint)
@@ -139,15 +161,35 @@ namespace WalletWasabi.Coins
}
}
+ internal void Undo(uint256 txId)
+ {
+ lock (Lock)
+ {
+ // remove recursively the coins created by the transaction
+ foreach (SmartCoin createdCoin in CreatedByNoLock(txId))
+ {
+ Coins.Remove(createdCoin);
+ }
+ // destroyed (spent) coins are now (unspent)
+ foreach (SmartCoin destroyedCoin in SpentByNoLock(txId))
+ {
+ if(SpentCoins.Remove(destroyedCoin))
+ {
+ Coins.Add(destroyedCoin);
+ }
+ }
+ }
+ }
+
public ICoinsView AsAllCoinsView()
{
lock (Lock)
{
- return new CoinsView(AsCoinsViewNoLock().Concat(SpentCoins).ToList());
+ return new CoinsView(AsAllCoinsViewNoLock().ToList());
}
}
- private ICoinsView AsAllCoinsViewNoLock() => new CoinsView(AsCoinsViewNoLock().Concat(SpentCoins).ToList());
+ private ICoinsView AsAllCoinsViewNoLock() => new CoinsView(AsCoinsViewNoLock().Concat(AsSpentCoinsViewNoLock()).ToList());
public ICoinsView AtBlockHeight(Height height) => AsCoinsView().AtBlockHeight(height);
@@ -175,7 +217,9 @@ namespace WalletWasabi.Coins
private ICoinsView CreatedByNoLock(uint256 txid) => AsCoinsViewNoLock().CreatedBy(txid);
- public ICoinsView SpentBy(uint256 txid) => AsCoinsView().SpentBy(txid);
+ public ICoinsView SpentBy(uint256 txid) => AsSpentCoinsView().SpentBy(txid);
+
+ private ICoinsView SpentByNoLock(uint256 txid) => AsSpentCoinsViewNoLock().SpentBy(txid);
public SmartCoin[] ToArray() => AsCoinsView().ToArray();

MODIFY
WalletWasabi/Transactions/TransactionProcessor.cs
WalletWasabi/Transactions/TransactionProcessor.cs
@@ -87,14 +87,15 @@ namespace WalletWasabi.Transactions
// 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.Remove(doubleSpentCoin);
- }
+ 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 destroyed
+ // ones. After undoing the replaced transaction it will process the replacement
+ // transaction.
+ var replacedTxId = doubleSpends.First().TransactionId;
+ Coins.Undo(replacedTxId);
tx.SetReplacement();
walletRelevant = true;
}

Fix RBF undoing process