-
Notifications
You must be signed in to change notification settings - Fork 492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix unnecessary inputs bug #9536
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI is failing.
WalletWasabi/Blockchain/TransactionBuilding/SmartCoinSelector.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. The only doubt that I have is in the case of lesser coins vs exact amount. If I want to pay 0.3btc and I have [0.2, 0.2, 0.1, 0.1] it prefers [0.2, 0.2] over [0.2, 0.1], what doesn't look 100% okay.
Anyway, the code makes sense. Just make sure this works okay with the BnB preselection.
I checked it, and AFAIK BnB logic is untouched by these changes.
So what I found about this scenario is: Order of the coins don't matter. In the test if we generate our coins this way:
It calculates and chooses the But if we generate our coins this way:
The So, while the order of the coins don't matter, how we call
the two newly generated coins will be linked together and So it works as intended IMO. |
@@ -88,20 +88,21 @@ public IEnumerable<ICoin> Select(IEnumerable<ICoin> suggestion, IMoney target) | |||
var coinsInBestClusterByScript = bestCoinCluster | |||
.GroupBy(c => c.ScriptPubKey) | |||
.Select(group => (ScriptPubKey: group.Key, Coins: group.ToList())) | |||
.OrderBy(x => x.Coins.Sum(c => c.Amount)) | |||
.OrderByDescending(x => x.Coins.Sum(c => c.Amount)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given how SmartCoinSelector
is commented, I would add a comment why descending ordering is the good thing to do here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking about an answer for this, and I just can't get it.
Short answer: it makes the tests pass.
Long answer: Something is fishy and I don't yet understand what.
#9536 (comment) - this sounds like a great thing to add to a single test. WDYT? |
LGTM. We can add short test summaries so that we won't spend time next time finding out what is covered and isn't. Up to you though. |
Sorry @kiminuo I'm not ignoring your questions and feedbacks, they are just hard to answer. 😅
The reason I didn't add this test (yet), because I don't fully understand why one scenario is failing and the other isn't. |
If you share those scenarios with me (ideally as two tests), then I can maybe help. |
So the main issue is this:
And today, I just can't make a passing test for this... The failing test looks like this:
Another failing test, where the selector would rather choose a
|
Yes! That's the answer. It selects the [0.2; 0.2] to prevent mixing unnecessary clusters and then the code was right before. The tests look good. |
Yep, took me some time to figure it out. 😅 Do the changes look good in The tests are passing, but we don't aim to satisfy the tests, we aim to have a nicely working coin selector. |
I plan to check it in an hour or so. Thanks! |
Look good to me. Personally, I would clean up those |
I will clean them up tomorrow. |
Personally, I would simplify & unify it as follows: using DynamicData;
using NBitcoin;
using System.Collections.Generic;
using System.Linq;
using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Blockchain.TransactionBuilding;
using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Tests.Helpers;
using Xunit;
namespace WalletWasabi.Tests.UnitTests.Wallet;
public class SmartCoinSelectorTests
{
public SmartCoinSelectorTests()
{
KeyManager = KeyManager.Recover(
new Mnemonic("all all all all all all all all all all all all"),
"",
Network.Main,
KeyManager.GetAccountKeyPath(Network.Main));
}
private KeyManager KeyManager { get; }
private static IEnumerable<Coin> EmptySuggestion { get; } = Enumerable.Empty<Coin>();
[Fact]
public void SelectsOnlyOneCoinWhenPossible()
{
Money target = Money.Coins(0.3m);
var availableCoins = GenerateSmartCoins(Enumerable.Range(0, 9).Select(i => ("Juan", 0.1m * (i + 1))));
var selector = new SmartCoinSelector(availableCoins);
var coinsToSpend = selector.Select(suggestion: EmptySuggestion, target);
var theOnlyOne = Assert.Single(coinsToSpend.Cast<Coin>());
Assert.Equal(0.3m, theOnlyOne.Amount.ToUnit(MoneyUnit.BTC));
}
[Fact]
public void DontSelectUnnecessaryInputs()
{
Money target = Money.Coins(4m);
List<SmartCoin> availableCoins = GenerateSmartCoins(Enumerable.Range(0, 10).Select(i => ("Juan", 0.1m * (i + 1))));
SmartCoinSelector selector = new(availableCoins);
List<Coin> coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(5, coinsToSpend.Count);
Assert.Equal(target, Money.Satoshis(coinsToSpend.Sum(x => x.Amount)));
}
[Fact]
public void PreferSameClusterOverExactAmount()
{
Money target = Money.Coins(0.3m);
List<SmartCoin> availableCoins = GenerateSmartCoins(("Besos", 0.2m), ("Besos", 0.2m), ("Juan", 0.1m), ("Juan", 0.1m));
SmartCoinSelector selector = new(availableCoins);
List<Coin> coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
// One might expect that we get an exact match of 0.3 BTC here - i.e. the combination ("Besos", 0.2m) and ("Juan", 0.1m), but that's
// not the case as we prefer not to mix clusters.
Assert.Equal(Money.Coins(0.4m), Money.Satoshis(coinsToSpend.Sum(x => x.Amount)));
}
[Fact]
public void PreferExactAmountWhenClustersAreDifferent()
{
Money target = Money.Coins(0.3m);
List<SmartCoin> availableCoins = GenerateSmartCoins(("Besos", 0.2m), ("Juan", 0.1m), ("Adam", 0.2m), ("Eve", 0.1m));
SmartCoinSelector selector = new(availableCoins);
List<Coin> coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(2, coinsToSpend.Count);
Assert.Equal(Money.Coins(0.3m), Money.Satoshis(coinsToSpend.Sum(x => x.Amount))); // Cluster-privacy is indifferent, so aim for exact amount.
}
[Fact]
public void DontUseTheWholeClusterIfNotNecessary()
{
Money target = Money.Coins(0.3m);
List<SmartCoin> availableCoins = GenerateDuplicateSmartCoins(("Juan", 0.1m), count: 10);
SmartCoinSelector selector = new(availableCoins);
List<Coin> coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(3, coinsToSpend.Count);
Assert.Equal(target, Money.Satoshis(coinsToSpend.Sum(x => x.Amount)));
}
[Fact]
public void PreferLessCoinsOnSameAmount()
{
Money target = Money.Coins(1m);
List<SmartCoin> availableCoins = GenerateDuplicateSmartCoins(("Juan", 0.1m), count: 11);
availableCoins.Add(GenerateDuplicateSmartCoins(("Beto", 0.2m), count: 5));
SmartCoinSelector selector = new(availableCoins);
List<Coin> coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(5, coinsToSpend.Count);
Assert.Equal(target, Money.Satoshis(coinsToSpend.Sum(x => x.Amount)));
}
[Fact]
public void PreferLessCoinsOverExactAmount()
{
Money target = Money.Coins(0.41m);
List<SmartCoin> smartCoins = GenerateSmartCoins(Enumerable.Range(0, 10).Select(i => ("Juan", 0.1m * (i + 1))));
smartCoins.Add(BitcoinFactory.CreateSmartCoin(smartCoins[0].HdPubKey, 0.11m));
var someCoins = smartCoins.Select(x => x.Coin);
var selector = new SmartCoinSelector(smartCoins);
var coinsToSpend = selector.Select(suggestion: someCoins, target);
var theOnlyOne = Assert.Single(coinsToSpend.Cast<Coin>());
Assert.Equal(0.5m, theOnlyOne.Amount.ToUnit(MoneyUnit.BTC));
}
[Fact]
public void PreferSameScript()
{
Money target = Money.Coins(0.31m);
var smartCoins = GenerateSmartCoins(Enumerable.Repeat(("Juan", 0.2m), 12));
smartCoins.Add(BitcoinFactory.CreateSmartCoin(smartCoins[0].HdPubKey, 0.11m));
var selector = new SmartCoinSelector(smartCoins);
var coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(2, coinsToSpend.Count);
Assert.Equal(coinsToSpend[0].ScriptPubKey, coinsToSpend[1].ScriptPubKey);
Assert.Equal(0.31m, coinsToSpend.Sum(x => x.Amount.ToUnit(MoneyUnit.BTC)));
}
[Fact]
public void PreferMorePrivateClusterScript()
{
Money target = Money.Coins(0.3m);
List<SmartCoin> coinsKnownByJuan = GenerateSmartCoins(("Juan", 0.2m), ("Juan", 0.2m), ("Juan", 0.2m), ("Juan", 0.2m), ("Juan", 0.2m));
List<SmartCoin> coinsKnownByBeto = GenerateSmartCoins(("Beto", 0.2m), ("Beto", 0.2m));
var selector = new SmartCoinSelector(coinsKnownByJuan.Concat(coinsKnownByBeto).ToList());
var coinsToSpend = selector.Select(suggestion: EmptySuggestion, target).Cast<Coin>().ToList();
Assert.Equal(2, coinsToSpend.Count);
Assert.Equal(0.4m, coinsToSpend.Sum(x => x.Amount.ToUnit(MoneyUnit.BTC)));
}
private List<SmartCoin> GenerateDuplicateSmartCoins((string Cluster, decimal amount) coin, int count)
=> GenerateSmartCoins(Enumerable.Range(start: 0, count).Select(x => coin));
private List<SmartCoin> GenerateSmartCoins(params (string Cluster, decimal amount)[] coins)
=> GenerateSmartCoins((IEnumerable<(string Cluster, decimal amount)>)coins);
private List<SmartCoin> GenerateSmartCoins(IEnumerable<(string Cluster, decimal amount)> coins)
{
Dictionary<string, List<(HdPubKey key, decimal amount)>> generatedKeyGroup = new();
// Create cluster-grouped keys
foreach (var targetCoin in coins)
{
var key = KeyManager.GenerateNewKey(new SmartLabel(targetCoin.Cluster), KeyState.Clean, false);
if (!generatedKeyGroup.ContainsKey(targetCoin.Cluster))
{
generatedKeyGroup.Add(targetCoin.Cluster, new());
}
generatedKeyGroup[targetCoin.Cluster].Add((key, targetCoin.amount));
}
var coinPairClusters = generatedKeyGroup.GroupBy(x => x.Key)
.Select(x => x.Select(y => y.Value)) // Group the coin pairs into clusters.
.SelectMany(x => x
.Select(coinPair => (coinPair,
cluster: new Cluster(coinPair.Select(z => z.key))))).ToList();
// Set each key with its corresponding cluster object.
foreach (var x in coinPairClusters)
{
foreach (var y in x.coinPair)
{
y.key.Cluster = x.cluster;
}
}
return coinPairClusters.Select(x => x.coinPair)
.SelectMany(x => x.Select(y => BitcoinFactory.CreateSmartCoin(y.key, y.amount)))
.ToList(); // Generate the final SmartCoins.
}
} |
* add failing test * fix the failing + existing tests * clean up the test * fix CI * use ThenBy() * add test * more precise tests * improve readability in tests * add more tests * fix tests * clean up and unify the tests
* add failing test * fix the failing + existing tests * clean up the test * fix CI * use ThenBy() * add test * more precise tests * improve readability in tests * add more tests * fix tests * clean up and unify the tests
Fixes: #9352