Make sure reorg coin list is the right one.
MODIFY
WalletWasabi.Tests/NodeBuilding/CoreNode.cs
WalletWasabi.Tests/NodeBuilding/CoreNode.cs
@@ -288,63 +288,6 @@ namespace WalletWasabi.Tests.NodeBuilding
return tasks.Select(b => b.GetAwaiter().GetResult()).ToArray();
}
- public async Task<Block[]> GenerateEmptyBlocksAsync(int height, BitcoinAddress minerAddress, int blockCount)
- {
- var rpc = CreateRpcClient();
- var bestBlock = await rpc.GetBlockAsync(height);
- ConcurrentChain chain = null;
- var blocks = new List<Block>();
- var now = MockTime ?? DateTimeOffset.UtcNow;
- using(var node = CreateNodeClient())
- {
- node.VersionHandshake();
- chain = node.GetChain(bestBlock.Header.GetHash());
-
- for(var i = 0; i < blockCount; i++)
- {
- uint nonce = 0;
- var block = rpc.Network.Consensus.ConsensusFactory.CreateBlock();
- block.Header.HashPrevBlock = chain.Tip.HashBlock;
- block.Header.Bits = block.Header.GetWorkRequired(rpc.Network, chain.Tip);
- block.Header.UpdateTime(now, rpc.Network, chain.Tip);
- var coinbase = new Transaction();
- coinbase.AddInput(TxIn.CreateCoinbase(chain.Height + 1));
- coinbase.AddOutput(new TxOut(rpc.Network.GetReward(chain.Height + 1), minerAddress));
- block.AddTransaction(coinbase);
- block.UpdateMerkleRoot();
- while(!block.CheckProofOfWork())
- block.Header.Nonce = ++nonce;
- blocks.Add(block);
- chain.SetTip(block.Header);
- }
- BroadcastBlocks(blocks.ToArray(), node);
- }
- return blocks.ToArray();
- }
-
- public async Task<Block[]> GenerateEmptyBlockAsync(int height, BitcoinAddress minerAddress, int blockCount)
- {
- var now = DateTimeOffset.UtcNow;
- var rpc = CreateRpcClient();
- var curBlock = await rpc.GetBlockAsync(height);
-
- var blocks = new List<Block>(blockCount);
-
- for (var i = 0; i < blockCount; i++)
- {
- var prevBlock = curBlock;
- var blockTime = now + TimeSpan.FromMinutes(i + 1);
- curBlock = curBlock.CreateNextBlockWithCoinbase(minerAddress, height + i + 1, blockTime);
- curBlock.Header.Bits = curBlock.Header.GetWorkRequired(rpc.Network, new ChainedBlock(prevBlock.Header, height + i + 1));
-
- MineBlock(curBlock);
- blocks.Add(curBlock);
- }
-
- BroadcastBlocks(blocks);
- return blocks.ToArray();
- }
-
private List<uint256> _toMalleate = new List<uint256>();
public void Malleate(uint256 txId)
{

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1181,7 +1181,7 @@ namespace WalletWasabi.Tests
// Get some money, make it confirm.
var key = wallet.GetReceiveKey("foo label");
- var fundingTxid = await Global.RpcClient.SendToAddressAsync(key.GetP2wpkhAddress(network), new Money(1m, MoneyUnit.BTC));
+ var fundingTxid = await Global.RpcClient.SendToAddressAsync(key.GetP2wpkhAddress(network), new Money(0.1m, MoneyUnit.BTC));
// Generate some coins
await Global.RpcClient.GenerateAsync(2);
@@ -1204,12 +1204,14 @@ namespace WalletWasabi.Tests
// Send money before reorg.
var operations = new[]{
- new WalletService.Operation(scp, Money.Coins(0.5m), "") };
- var btx = await wallet.BuildTransactionAsync("password", operations, 2);
+ new WalletService.Operation(scp, Money.Coins(0.011m), "") };
+ var btx1 = await wallet.BuildTransactionAsync("password", operations, 2);
+ await wallet.SendTransactionAsync(btx1.Transaction);
operations = new[]{
- new WalletService.Operation(scp, Money.Coins(0.5m), "") };
- btx = await wallet.BuildTransactionAsync("password", operations, 2);
+ new WalletService.Operation(scp, Money.Coins(0.012m), "") };
+ var btx2 = await wallet.BuildTransactionAsync("password", operations, 2, allowUnconfirmed: true);
+ await wallet.SendTransactionAsync(btx2.Transaction);
// Test synchronization after fork.
// Invalidate the blocks containing the funding transaction
@@ -1218,7 +1220,7 @@ namespace WalletWasabi.Tests
tip = await Global.RpcClient.GetBestBlockHashAsync();
await Global.RpcClient.InvalidateBlockAsync(tip); // Reorg 2
- // Generate two new blocks (replace the previous invalidated ones)
+ // Generate three new blocks (replace the previous invalidated ones)
_filtersProcessedByWalletCount = 0;
await Global.RpcClient.GenerateAsync(3);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 3);
@@ -1227,24 +1229,24 @@ namespace WalletWasabi.Tests
// When we invalidate a block, those transactions setted in the invalidated block
// are reintroduced when we generate a new block though the rpc call
operations = new[]{
- new WalletService.Operation(scp, Money.Coins(0.5m), "") };
- btx = await wallet.BuildTransactionAsync("password", operations, 2);
+ new WalletService.Operation(scp, Money.Coins(0.013m), "") };
+ var btx3 = await wallet.BuildTransactionAsync("password", operations, 2);
+ await wallet.SendTransactionAsync(btx3.Transaction);
operations = new[]{
- new WalletService.Operation(scp, Money.Coins(0.5m), "") };
- btx = await wallet.BuildTransactionAsync("password", operations, 2);
-
+ new WalletService.Operation(scp, Money.Coins(0.014m), "") };
+ var btx4 = await wallet.BuildTransactionAsync("password", operations, 2, allowUnconfirmed: true);
+ await wallet.SendTransactionAsync(btx4.Transaction);
// Test synchronization after fork with different transactions.
// Create a fork that invalidates the blocks containing the funding transaction
- await Global.RpcClient.InvalidateBlockAsync(baseTip);
- var winningFork = await RegTestFixture.BackendRegTestNode.GenerateEmptyBlocksAsync(100,
- new Key().PubKey.GetAddress(Global.RpcClient.Network), 10);
-
- tip = await Global.RpcClient.GetBestBlockHashAsync();
- Assert.Equal(tip, winningFork.Last().GetHash());
-
_filtersProcessedByWalletCount = 0;
+ var bc1 = Global.RpcClient.GetBlockCount();
+ await Global.RpcClient.InvalidateBlockAsync(baseTip);
+ var bc2 = Global.RpcClient.GetBlockCount();
+ await Global.RpcClient.SendCommandAsync("abandontransaction", fundingTxid.ToString());
+ await Global.RpcClient.GenerateAsync(10);
+ var bc3 = Global.RpcClient.GetBlockCount();
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 10);
var curBlockHash = await Global.RpcClient.GetBestBlockHashAsync();
@@ -1278,8 +1280,8 @@ namespace WalletWasabi.Tests
Assert.Single(wallet.Coins.Where(x => x.TransactionId == fundingBumpTxid.TransactionId));
// Confirm the coin
- await Global.RpcClient.GenerateAsync(1);
_filtersProcessedByWalletCount = 0;
+ await Global.RpcClient.GenerateAsync(1);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);
// There shouldn't be any `confirmed` coin
@@ -1293,7 +1295,6 @@ namespace WalletWasabi.Tests
// Dispose connection service.
nodes?.Dispose();
}
-
}
#endregion

MODIFY
WalletWasabi.Tests/SmartModelTests.cs
WalletWasabi.Tests/SmartModelTests.cs
@@ -33,16 +33,16 @@ namespace WalletWasabi.Tests
var height = Height.MemPool;
var label = "foo";
- var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, label, txId);
+ var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, label, txId);
// If the txid or the index differs, equality should think it's a different coin.
- var differentCoin = new SmartCoin(txId, index + 1, scriptPubKey, amount, spentOutputs, height, label, txId);
+ var differentCoin = new SmartCoin(txId, index + 1, scriptPubKey, amount, spentOutputs, height, tx.RBF, label, txId);
var differentOutput = tx.Outputs[1];
var differentSpentOutputs = new[]
{
new TxoRef(txId, 0)
};
// If the txid and the index is the same, equality should think it's the same coin.
- var sameCoin = new SmartCoin(txId, index, differentOutput.ScriptPubKey, differentOutput.Value, differentSpentOutputs, Height.Unknown, "boo", null);
+ var sameCoin = new SmartCoin(txId, index, differentOutput.ScriptPubKey, differentOutput.Value, differentSpentOutputs, Height.Unknown, tx.RBF, "boo", null);
Assert.Equal(coin, sameCoin);
Assert.NotEqual(coin, differentCoin);
@@ -61,7 +61,7 @@ namespace WalletWasabi.Tests
var height = Height.MemPool;
var label = "foo";
- var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, label, txId);
+ var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, label, txId);
var serialized = JsonConvert.SerializeObject(coin);
Debug.WriteLine(serialized);

MODIFY
WalletWasabi/Models/SmartCoin.cs
WalletWasabi/Models/SmartCoin.cs
@@ -45,11 +45,15 @@ namespace WalletWasabi.Models
[JsonProperty(Order = 8)]
public TxoRef[] SpentOutputs { get; }
+ [JsonProperty(Order = 9)]
+ [JsonConverter(typeof(FunnyBoolConverter))]
+ public bool RBF { get; }
+
public bool Unspent => SpenderTransactionId == null;
public bool Confirmed => Height != Height.MemPool && Height != Height.Unknown;
[JsonConstructor]
- public SmartCoin(uint256 transactionId, int index, Script scriptPubKey, Money amount, TxoRef[] spentOutputs, Height height, string label = "", uint256 spenderTransactionId = null)
+ public SmartCoin(uint256 transactionId, int index, Script scriptPubKey, Money amount, TxoRef[] spentOutputs, Height height, bool rbf, string label = "", uint256 spenderTransactionId = null)
{
TransactionId = Guard.NotNull(nameof(transactionId), transactionId);
Index = Guard.NotNull(nameof(index), index);
@@ -59,6 +63,7 @@ namespace WalletWasabi.Models
Height = height;
Label = Guard.Correct(label);
SpenderTransactionId = spenderTransactionId;
+ RBF = rbf;
}
public Coin ToCoin()

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -253,68 +253,83 @@ namespace WalletWasabi.Services
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();
}
- // Check double spending tx (invalid transactions has to be rejected)
- var spentTxOuts = Coins.SelectMany(x=>x.SpentOutputs);
- var txSpendingOutputs = tx.Transaction.Inputs.ToTxoRefs();
-
- var alreadySpentOutputs = spentTxOuts.Intersect(txSpendingOutputs).ToArray();
-
- // if the received tx spends any already-spent outputs and this is a RBF tx
- // then remove the coins in order to replace them for those in the new tx (RBF)
- if (alreadySpentOutputs.Any())
+ 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)
{
- if (tx.Transaction.RBF)
+ 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)
{
- foreach (var spentOutput in alreadySpentOutputs)
+ if (tx.Height == Height.MemPool)
{
- var coinsToreplace = Coins.Where(x => x.SpentOutputs.Contains(spentOutput)).ToArray();
- for (var j = 0; j < coinsToreplace.Length; j++)
+ // if all double spent coins are mempool and RBF
+ if (doubleSpends.All(x => x.Height == Height.MemPool && x.RBF))
{
- if (!Coins.TryRemove(coinsToreplace[j]))
+ // 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)
{
- // what here? retry?
+ RemoveCoinRecursively(doubleSpentCoin);
}
}
+ else
+ {
+ return;
}
}
- else // tx is double spending at least one output
+ else // new confirmation always enjoys priority
{
- // just discard it
- return;
+ // 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);
+ }
}
}
- // If transaction received to any of the wallet keys:
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)
{
- // 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);
+ var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.Transaction.RBF, foundKey.Label, null);
Coins.Add(coin);
// Make sure there's always 21 clean keys generated and indexed.
@@ -725,7 +740,7 @@ namespace WalletWasabi.Services
for (var i = 0; i < tx.Outputs.Count; i++)
{
TxOut output = tx.Outputs[i];
- var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, spentOutputs, Height.Unknown);
+ var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, spentOutputs, Height.Unknown, tx.RBF);
if (KeyManager.GetKeys(KeyState.Clean).Select(x => x.GetP2wpkhScript()).Contains(coin.ScriptPubKey))
{
coin.Label = changeLabel;

Make sure reorg coin list is the right one.