Adjust gap limit for too many receive instead of recycling.
MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/ReceiveTabViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/ReceiveTabViewModel.cs
@@ -53,26 +53,22 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
if (label.IsEmpty)
{
NotificationHelpers.Warning("Label is required.");
-
return;
}
- Dispatcher.UIThread.PostLogException(() =>
+ AvaloniaThreadingExtensions.PostLogException(Dispatcher.UIThread, () =>
{
- HdPubKey newKey = Global.WalletService.GetReceiveKey(_labelSuggestion.Label, Addresses.Select(x => x.Model).Take(7)); // Never touch the first 7 keys.
-
- AddressViewModel found = Addresses.FirstOrDefault(x => x.Model == newKey);
- if (found != default)
+ var newKey = KeyManager.GetNextReceiveKey(label, out bool minGapLimitIncreased);
+ if (minGapLimitIncreased)
{
- Addresses.Remove(found);
+ int minGapLimit = KeyManager.MinGapLimit.Value;
+ int prevMinGapLimit = minGapLimit - 1;
+ NotificationHelpers.Warning($"{nameof(KeyManager.MinGapLimit)} increased from {prevMinGapLimit} to {minGapLimit}.");
}
var newAddress = new AddressViewModel(newKey, Global);
-
Addresses.Insert(0, newAddress);
-
SelectedAddress = newAddress;
-
_labelSuggestion.Label = "";
});
});

MODIFY
WalletWasabi.Tests/IntegrationTests/RegTests.cs
WalletWasabi.Tests/IntegrationTests/RegTests.cs
@@ -479,7 +479,7 @@ namespace WalletWasabi.Tests.IntegrationTests
wallet.NewFilterProcessed += Wallet_NewFilterProcessed;
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(0.1m));
await rpc.GenerateAsync(1);
@@ -517,7 +517,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.Equal("foo label", keyManager.GetKeys(KeyState.Used, false).Single().Label);
// Get some money, make it confirm.
- var key2 = wallet.GetReceiveKey("bar label");
+ var key2 = keyManager.GetNextReceiveKey("bar label", out _);
var txId2 = await rpc.SendToAddressAsync(key2.GetP2wpkhAddress(network), Money.Coins(0.01m));
Interlocked.Exchange(ref _filtersProcessedByWalletCount, 0);
await rpc.GenerateAsync(1);
@@ -699,8 +699,8 @@ namespace WalletWasabi.Tests.IntegrationTests
wallet.NewFilterProcessed += Wallet_NewFilterProcessed;
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
- var key2 = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
+ var key2 = keyManager.GetNextReceiveKey("foo label", out _);
var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m));
Assert.NotNull(txId);
await rpc.GenerateAsync(1);
@@ -760,7 +760,7 @@ namespace WalletWasabi.Tests.IntegrationTests
#region Basic
- Script receive = wallet.GetReceiveKey("Basic").P2wpkhScript;
+ Script receive = keyManager.GetNextReceiveKey("Basic", out _).P2wpkhScript;
Money amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 2;
var res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);
@@ -808,7 +808,7 @@ namespace WalletWasabi.Tests.IntegrationTests
#region SubtractFeeFromAmount
- receive = wallet.GetReceiveKey("SubtractFeeFromAmount").P2wpkhScript;
+ receive = keyManager.GetNextReceiveKey("SubtractFeeFromAmount", out _).P2wpkhScript;
amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3;
res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, subtractFee: true, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);
@@ -947,7 +947,7 @@ namespace WalletWasabi.Tests.IntegrationTests
#region MaxAmount
- receive = wallet.GetReceiveKey("MaxAmount").P2wpkhScript;
+ receive = keyManager.GetNextReceiveKey("MaxAmount", out _).P2wpkhScript;
res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);
@@ -968,7 +968,7 @@ namespace WalletWasabi.Tests.IntegrationTests
#region InputSelection
- receive = wallet.GetReceiveKey("InputSelection").P2wpkhScript;
+ receive = keyManager.GetNextReceiveKey("InputSelection", out _).P2wpkhScript;
var inputCountBefore = res.SpentCoins.Count();
@@ -1008,7 +1008,7 @@ namespace WalletWasabi.Tests.IntegrationTests
#region Labeling
- Script receive2 = wallet.GetReceiveKey(SmartLabel.Empty).P2wpkhScript;
+ Script receive2 = keyManager.GetNextReceiveKey("foo", out _).P2wpkhScript;
res = wallet.BuildTransaction(password, new PaymentIntent(receive2, MoneyRequest.CreateAllRemaining(), "my label"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);
Assert.Single(res.InnerWalletOutputs);
@@ -1057,7 +1057,7 @@ namespace WalletWasabi.Tests.IntegrationTests
inputCountBefore = res.SpentCoins.Count();
- receive = wallet.GetReceiveKey("AllowedInputsDisallowUnconfirmed").P2wpkhScript;
+ receive = keyManager.GetNextReceiveKey("AllowedInputsDisallowUnconfirmed", out _).P2wpkhScript;
var allowedInputs = wallet.Coins.Where(x => !x.Unavailable).Select(x => new TxoRef(x.TransactionId, x.Index)).Take(1);
var toSend = new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "fizz");
@@ -1234,7 +1234,7 @@ namespace WalletWasabi.Tests.IntegrationTests
false));
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m));
// Generate some coins
@@ -1355,7 +1355,7 @@ namespace WalletWasabi.Tests.IntegrationTests
var scp = new Key().ScriptPubKey;
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
var fundingTxId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(0.1m));
// Generate some coins
@@ -1457,7 +1457,7 @@ namespace WalletWasabi.Tests.IntegrationTests
var overwriteTx = Transaction.Create(network);
overwriteTx.Inputs.AddRange(invalidSmartTransaction.Transaction.Inputs);
- var walletAddress = wallet.GetReceiveKey("").GetP2wpkhAddress(network);
+ var walletAddress = keyManager.GetNextReceiveKey("foo", out _).GetP2wpkhAddress(network);
bool onAddress = false;
foreach (var invalidOutput in invalidSmartTransaction.Transaction.Outputs)
{
@@ -1575,7 +1575,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.Empty(wallet.Coins);
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
try
{
@@ -1695,7 +1695,7 @@ namespace WalletWasabi.Tests.IntegrationTests
// Test coin basic count.
ICoinsView GetAllCoins() => wallet.TransactionProcessor.Coins.AsAllCoinsView();
var coinCount = GetAllCoins().Count();
- var to = wallet.GetReceiveKey("foo");
+ var to = keyManager.GetNextReceiveKey("foo", out _);
var res = wallet.BuildTransaction(password, new PaymentIntent(to.P2wpkhScript, Money.Coins(0.2345m), label: "bar"), FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true);
await broadcaster.SendTransactionAsync(res.Transaction);
Assert.Equal(coinCount + 2, GetAllCoins().Count());
@@ -1755,7 +1755,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.Empty(wallet.Coins);
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("foo label");
+ var key = keyManager.GetNextReceiveKey("foo label", out _);
try
{
@@ -3382,12 +3382,12 @@ namespace WalletWasabi.Tests.IntegrationTests
var wallet2 = new WalletService(bitcoinStore, keyManager2, synchronizer2, chaumianClient2, nodes2, workDir2, serviceConfiguration, synchronizer2);
// Get some money, make it confirm.
- var key = wallet.GetReceiveKey("fundZeroLink");
+ var key = keyManager.GetNextReceiveKey("fundZeroLink", out _);
var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m));
Assert.NotNull(txId);
- var key2 = wallet2.GetReceiveKey("fundZeroLink");
- var key3 = wallet2.GetReceiveKey("fundZeroLink");
- var key4 = wallet2.GetReceiveKey("fundZeroLink");
+ var key2 = keyManager2.GetNextReceiveKey("fundZeroLink", out _);
+ var key3 = keyManager2.GetNextReceiveKey("fundZeroLink", out _);
+ var key4 = keyManager2.GetNextReceiveKey("fundZeroLink", out _);
var txId2 = await rpc.SendToAddressAsync(key2.GetP2wpkhAddress(network), Money.Coins(0.11m));
var txId3 = await rpc.SendToAddressAsync(key3.GetP2wpkhAddress(network), Money.Coins(0.12m));
var txId4 = await rpc.SendToAddressAsync(key4.GetP2wpkhAddress(network), Money.Coins(0.13m));

MODIFY
WalletWasabi/Blockchain/Keys/KeyManager.cs
WalletWasabi/Blockchain/Keys/KeyManager.cs
@@ -452,6 +452,35 @@ namespace WalletWasabi.Blockchain.Keys
}
}
+ public HdPubKey GetNextReceiveKey(SmartLabel label, out bool minGapLimitIncreased)
+ {
+ if (label.IsEmpty)
+ {
+ throw new InvalidOperationException("Label is required.");
+ }
+
+ minGapLimitIncreased = false;
+
+ AssertCleanKeysIndexed(isInternal: false);
+
+ // Find the next clean external key with empty label.
+ var newKey = GetKeys(x => x.IsInternal == false && x.KeyState == KeyState.Clean && x.Label.IsEmpty).FirstOrDefault();
+
+ // If not found, generate a new.
+ if (newKey is null)
+ {
+ SetMinGapLimit(MinGapLimit.Value + 1);
+ newKey = AssertCleanKeysIndexed(isInternal: false).First();
+
+ // If the new is over the MinGapLimit, set minGapLimitIncreased to true.
+ minGapLimitIncreased = true;
+ }
+
+ newKey.SetLabel(label, kmToFile: this);
+
+ return newKey;
+ }
+
public IEnumerable<HdPubKey> GetKeys(Func<HdPubKey, bool> wherePredicate)
{
// BIP44-ish derivation scheme
@@ -582,38 +611,35 @@ namespace WalletWasabi.Blockchain.Keys
/// Make sure there's always clean keys generated and indexed.
/// Call SetMinGapLimit() to set how many keys should be asserted.
/// </summary>
- public bool AssertCleanKeysIndexed(bool? isInternal = null)
+ public IEnumerable<HdPubKey> AssertCleanKeysIndexed(bool? isInternal = null)
{
- var generated = false;
+ var newNewKeys = new List<HdPubKey>();
if (isInternal is null)
{
while (GetKeys(KeyState.Clean, true).Count() < MinGapLimit)
{
- GenerateNewKey(SmartLabel.Empty, KeyState.Clean, true, toFile: false);
- generated = true;
+ newNewKeys.Add(GenerateNewKey(SmartLabel.Empty, KeyState.Clean, true, toFile: false));
}
while (GetKeys(KeyState.Clean, false).Count() < MinGapLimit)
{
- GenerateNewKey(SmartLabel.Empty, KeyState.Clean, false, toFile: false);
- generated = true;
+ newNewKeys.Add(GenerateNewKey(SmartLabel.Empty, KeyState.Clean, false, toFile: false));
}
}
else
{
while (GetKeys(KeyState.Clean, isInternal).Count() < MinGapLimit)
{
- GenerateNewKey(SmartLabel.Empty, KeyState.Clean, (bool)isInternal, toFile: false);
- generated = true;
+ newNewKeys.Add(GenerateNewKey(SmartLabel.Empty, KeyState.Clean, (bool)isInternal, toFile: false));
}
}
- if (generated)
+ if (newNewKeys.Any())
{
ToFile();
}
- return generated;
+ return newNewKeys;
}
/// <summary>

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -353,29 +353,6 @@ namespace WalletWasabi.Services
NewBlockProcessed?.Invoke(this, currentBlock);
}
- public HdPubKey GetReceiveKey(SmartLabel label, IEnumerable<HdPubKey> dontTouch = null)
- {
- // Make sure there's always 21 clean keys generated and indexed.
- KeyManager.AssertCleanKeysIndexed(isInternal: false);
-
- IEnumerable<HdPubKey> keys = KeyManager.GetKeys(KeyState.Clean, isInternal: false);
- if (dontTouch != null)
- {
- keys = keys.Except(dontTouch);
- if (!keys.Any())
- {
- throw new InvalidOperationException($"{nameof(dontTouch)} covers all the possible keys.");
- }
- }
-
- var foundLabelless = keys.FirstOrDefault(x => x.Label.IsEmpty); // Return the first labelless.
- HdPubKey ret = foundLabelless ?? keys.RandomElement(); // Return the first, because that's the oldest.
-
- ret.SetLabel(label, KeyManager);
-
- return ret;
- }
-
private Node _localBitcoinCoreNode = null;
public Node LocalBitcoinCoreNode

Adjust gap limit for too many receive instead of recycling.