Implement mixin
MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1439,9 +1439,18 @@ namespace WalletWasabi.Tests
await Task.Delay(500); // Waits for the funding transaction get to the mempool.
Assert.Single(wallet.Coins);
- // Spend the unconfirmed coin (send it to ourself)
- var operations = new[] { new WalletService.Operation(key.PubKey.WitHash.ScriptPubKey, Money.Coins(0.5m), "") };
+ // Test mixin
+ var operations = new[] {
+ new WalletService.Operation(key.GetP2wpkhScript(), Money.Coins(0.01m), ""),
+ new WalletService.Operation(new Key().ScriptPubKey, Money.Coins(0.01m), ""),
+ new WalletService.Operation(new Key().ScriptPubKey, Money.Coins(0.01m), "")
+ };
var tx1Res = await wallet.BuildTransactionAsync(password, operations, 2, allowUnconfirmed: true);
+ Assert.Equal(2, tx1Res.OuterWalletOutputs.Single(x => x.ScriptPubKey == key.GetP2wpkhScript()).Mixin);
+
+ // Spend the unconfirmed coin (send it to ourself)
+ operations = new[] { new WalletService.Operation(key.PubKey.WitHash.ScriptPubKey, Money.Coins(0.5m), "") };
+ tx1Res = await wallet.BuildTransactionAsync(password, operations, 2, allowUnconfirmed: true);
await wallet.SendTransactionAsync(tx1Res.Transaction);
while (wallet.Coins.Count != 3)
@@ -1621,7 +1630,7 @@ namespace WalletWasabi.Tests
var addr = await rpc.GetNewAddressAsync();
var hash = await rpc.SendToAddressAsync(addr, new Money(0.01m, MoneyUnit.BTC));
var tx = await rpc.GetRawTransactionAsync(hash);
- var index = tx.Outputs.GetIndex(addr.ScriptPubKey);
+ var index = tx.Outputs.GetIndexes(addr.ScriptPubKey).Single();
inputsRequest.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(hash, index), Proof = "b" } };
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await AliceClient.CreateNewAsync(inputsRequest, baseUri));
@@ -1643,7 +1652,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
+ index = tx.Outputs.GetIndexes(witnessAddress.ScriptPubKey).Single();
inputsRequest.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(hash, index), Proof = "b" } };
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await AliceClient.CreateNewAsync(inputsRequest, baseUri));
Assert.StartsWith($"{HttpStatusCode.BadRequest.ToReasonString()}", httpRequestException.Message);
@@ -1737,7 +1746,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
+ index = tx.Outputs.GetIndexes(witnessAddress.ScriptPubKey).Single();
proof = key.SignMessage(inputsRequest.BlindedOutputScriptHex);
inputProofs.Add(new InputProofModel { Input = new OutPoint(hash, index), Proof = proof });
}
@@ -1811,7 +1820,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
+ index = tx.Outputs.GetIndexes(witnessAddress.ScriptPubKey).Single();
scriptBytes = new Key().PubKey.GetSegwitAddress(network).ScriptPubKey.ToBytes();
(BlindingFactor, BlindedData) = blindingKey.PubKey.Blind(scriptBytes);
@@ -2063,7 +2072,7 @@ namespace WalletWasabi.Tests
Assert.NotNull(txHash);
Transaction transaction = await rpc.GetRawTransactionAsync(txHash);
- var outputIndex = transaction.Outputs.GetIndex(inputAddress.ScriptPubKey);
+ var outputIndex = transaction.Outputs.GetIndexes(inputAddress.ScriptPubKey).Single();
OutPoint input = new OutPoint(txHash, outputIndex);
var inputProof = new InputProofModel { Input = input, Proof = key.SignMessage(ByteHelpers.ToHex(blinded.BlindedData)) };
@@ -2260,9 +2269,9 @@ namespace WalletWasabi.Tests
var tx3 = await rpc.GetRawTransactionAsync(txid3);
await rpc.GenerateAsync(1);
var height = await rpc.GetBlockCountAsync();
- var smartCoin1 = new SmartCoin(txid1, tx1.Outputs.GetIndex(bech1.ScriptPubKey), bech1.ScriptPubKey, amount1, tx1.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
- var smartCoin2 = new SmartCoin(txid2, tx2.Outputs.GetIndex(bech2.ScriptPubKey), bech2.ScriptPubKey, amount2, tx2.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
- var smartCoin3 = new SmartCoin(txid3, tx3.Outputs.GetIndex(bech3.ScriptPubKey), bech3.ScriptPubKey, amount3, tx3.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
+ var smartCoin1 = new SmartCoin(txid1, tx1.Outputs.GetIndexes(bech1.ScriptPubKey).Single(), bech1.ScriptPubKey, amount1, tx1.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx1.GetMixin(tx1.Outputs.GetIndexes(bech1.ScriptPubKey).Single()));
+ var smartCoin2 = new SmartCoin(txid2, tx2.Outputs.GetIndexes(bech2.ScriptPubKey).Single(), bech2.ScriptPubKey, amount2, tx2.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx2.GetMixin(tx2.Outputs.GetIndexes(bech2.ScriptPubKey).Single()));
+ var smartCoin3 = new SmartCoin(txid3, tx3.Outputs.GetIndexes(bech3.ScriptPubKey).Single(), bech3.ScriptPubKey, amount3, tx3.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx3.GetMixin(tx3.Outputs.GetIndexes(bech3.ScriptPubKey).Single()));
var chaumianClient1 = new CcjClient(rpc.Network, coordinator.RsaKey.PubKey, keyManager, new Uri(RegTestFixture.BackendEndPoint));
var chaumianClient2 = new CcjClient(rpc.Network, coordinator.RsaKey.PubKey, keyManager, new Uri(RegTestFixture.BackendEndPoint));
@@ -2285,7 +2294,7 @@ namespace WalletWasabi.Tests
Assert.True(smartCoin2.Locked);
// Make sure it doesn't throw.
- await chaumianClient1.DequeueCoinsFromMixAsync(new SmartCoin((new Transaction()).GetHash(), 1, new Script(), Money.Parse("3"), new TxoRef[] { new TxoRef((new Transaction()).GetHash(), 0) }, Height.MemPool, rbf: false));
+ await chaumianClient1.DequeueCoinsFromMixAsync(new SmartCoin((new Transaction()).GetHash(), 1, new Script(), Money.Parse("3"), new TxoRef[] { new TxoRef((new Transaction()).GetHash(), 0) }, Height.MemPool, rbf: false, mixin: 0));
Assert.True(2 == (await chaumianClient1.QueueCoinsToMixAsync(password, smartCoin1, smartCoin2)).Count());
await chaumianClient1.DequeueCoinsFromMixAsync(smartCoin1);
@@ -2368,9 +2377,9 @@ namespace WalletWasabi.Tests
var tx3 = await rpc.GetRawTransactionAsync(txid3);
await rpc.GenerateAsync(1);
var height = await rpc.GetBlockCountAsync();
- var smartCoin1 = new SmartCoin(txid1, tx1.Outputs.GetIndex(bech1.ScriptPubKey), bech1.ScriptPubKey, amount1, tx1.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
- var smartCoin2 = new SmartCoin(txid2, tx2.Outputs.GetIndex(bech2.ScriptPubKey), bech2.ScriptPubKey, amount2, tx2.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
- var smartCoin3 = new SmartCoin(txid3, tx3.Outputs.GetIndex(bech3.ScriptPubKey), bech3.ScriptPubKey, amount3, tx3.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
+ var smartCoin1 = new SmartCoin(txid1, tx1.Outputs.GetIndexes(bech1.ScriptPubKey).Single(), bech1.ScriptPubKey, amount1, tx1.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx1.GetMixin(tx1.Outputs.GetIndexes(bech1.ScriptPubKey).Single()));
+ var smartCoin2 = new SmartCoin(txid2, tx2.Outputs.GetIndexes(bech2.ScriptPubKey).Single(), bech2.ScriptPubKey, amount2, tx2.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx2.GetMixin(tx2.Outputs.GetIndexes(bech2.ScriptPubKey).Single()));
+ var smartCoin3 = new SmartCoin(txid3, tx3.Outputs.GetIndexes(bech3.ScriptPubKey).Single(), bech3.ScriptPubKey, amount3, tx3.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false, mixin: tx3.GetMixin(tx3.Outputs.GetIndexes(bech3.ScriptPubKey).Single()));
var chaumianClient1 = new CcjClient(rpc.Network, coordinator.RsaKey.PubKey, keyManager, new Uri(RegTestFixture.BackendEndPoint));
var chaumianClient2 = new CcjClient(rpc.Network, coordinator.RsaKey.PubKey, keyManager, new Uri(RegTestFixture.BackendEndPoint));

MODIFY
WalletWasabi.Tests/SmartModelTests.cs
WalletWasabi.Tests/SmartModelTests.cs
@@ -30,16 +30,16 @@ namespace WalletWasabi.Tests
var height = Height.MemPool;
var label = "foo";
- var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, label, txId);
+ var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, tx.GetMixin(index), 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, tx.RBF, label, txId);
+ var differentCoin = new SmartCoin(txId, index + 1, scriptPubKey, amount, spentOutputs, height, tx.RBF, tx.GetMixin(index + 1), 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, tx.RBF, "boo", null);
+ var sameCoin = new SmartCoin(txId, index, differentOutput.ScriptPubKey, differentOutput.Value, differentSpentOutputs, Height.Unknown, tx.RBF, tx.GetMixin(index), "boo", null);
Assert.Equal(coin, sameCoin);
Assert.NotEqual(coin, differentCoin);
@@ -58,7 +58,7 @@ namespace WalletWasabi.Tests
var height = Height.MemPool;
var label = "foo";
- var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, label, txId);
+ var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, tx.GetMixin(index), label, txId);
var serialized = JsonConvert.SerializeObject(coin);
Debug.WriteLine(serialized);

MODIFY
WalletWasabi/Extensions/NBitcoinExtensions.cs
WalletWasabi/Extensions/NBitcoinExtensions.cs
@@ -18,23 +18,23 @@ namespace NBitcoin
}
}
- public static int GetIndex(this TxOutList me, Script script)
+ public static IEnumerable<int> GetIndexes(this TxOutList me, Script script)
{
- var index = -1;
+ var indexes = new List<int>();
for (int i = 0; i < me.Count; i++)
{
var output = me[i];
if (output.ScriptPubKey == script)
{
- index = i;
+ indexes.Add(i);
}
}
- if (index == -1)
+ if (indexes.Count == 0)
{
throw new InvalidOperationException("Script not found.");
}
- return index;
+ return indexes;
}
public static string ToHex(this IBitcoinSerializable me)
@@ -58,5 +58,17 @@ namespace NBitcoin
return ret;
}
+
+ public static int GetAnonymitySet(this Transaction me, int outputIndex)
+ {
+ var output = me.Outputs[outputIndex];
+ return me.GetIndistinguishableOutputs().Single(x => x.value == output.Value).count;
+ }
+
+ public static int GetMixin(this Transaction me, int outputIndex)
+ {
+ var output = me.Outputs[outputIndex];
+ return me.GetIndistinguishableOutputs().Single(x => x.value == output.Value).count - 1;
+ }
}
}

MODIFY
WalletWasabi/Models/SmartCoin.cs
WalletWasabi/Models/SmartCoin.cs
@@ -53,6 +53,9 @@ namespace WalletWasabi.Models
[JsonConverter(typeof(FunnyBoolJsonConverter))]
public bool Locked { get; set; }
+ [JsonProperty(Order = 11)]
+ public int Mixin { get; }
+
public bool SpentOrLocked => SpenderTransactionId != null || Locked;
public bool Unspent => SpenderTransactionId == null;
public bool Confirmed => Height != Height.MemPool && Height != Height.Unknown;
@@ -63,13 +66,14 @@ namespace WalletWasabi.Models
public ISecret Secret { get; set; }
[JsonConstructor]
- public SmartCoin(uint256 transactionId, int index, Script scriptPubKey, Money amount, TxoRef[] spentOutputs, Height height, bool rbf, string label = "", uint256 spenderTransactionId = null, bool locked = false)
+ public SmartCoin(uint256 transactionId, int index, Script scriptPubKey, Money amount, TxoRef[] spentOutputs, Height height, bool rbf, int mixin, string label = "", uint256 spenderTransactionId = null, bool locked = false)
{
TransactionId = Guard.NotNull(nameof(transactionId), transactionId);
Index = Guard.NotNull(nameof(index), index);
ScriptPubKey = Guard.NotNull(nameof(scriptPubKey), scriptPubKey);
Amount = Guard.NotNull(nameof(amount), amount);
SpentOutputs = Guard.NotNull(nameof(spentOutputs), spentOutputs);
+ Mixin = Guard.InRangeAndNotNull(nameof(mixin), mixin, 0, int.MaxValue);
Height = height;
Label = Guard.Correct(label);
SpenderTransactionId = spenderTransactionId;

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -336,7 +336,13 @@ namespace WalletWasabi.Services
if (foundKey != default)
{
foundKey.SetKeyState(KeyState.Used, KeyManager);
- 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.
+ 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.Add(coin);
if (coin.Unspent && coin.Label == "ZeroLink Change" && ChaumianClient.OnePiece != null)
{
@@ -761,7 +767,8 @@ 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, tx.RBF);
+ var mixin = tx.GetMixin(i) + spentCoins.Min(x => x.Mixin);
+ var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Inputs.ToTxoRefs().ToArray(), Height.Unknown, tx.RBF, mixin);
if (KeyManager.GetKeys(KeyState.Clean).Select(x => x.GetP2wpkhScript()).Contains(coin.ScriptPubKey))
{
coin.Label = changeLabel;

Implement mixin