Sort inputs and outputs
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PostRequests/SignTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PostRequests/SignTransactionTests.cs
@@ -72,31 +72,5 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend.PostRequests
}
await arena.StopAsync(CancellationToken.None);
}
-
- [Fact]
- public async Task WrongCoinjoinSignatureAsync()
- {
- WabiSabiConfig cfg = new();
- var round = WabiSabiFactory.CreateRound(cfg);
- using Key key1 = new();
- Alice alice1 = WabiSabiFactory.CreateAlice(key: key1, round: round);
- using Key key2 = new();
- Alice alice2 = WabiSabiFactory.CreateAlice(key: key2, round: round);
- round.Alices.Add(alice1);
- round.Alices.Add(alice2);
- round.CoinjoinState = round.Assert<ConstructionState>().AddInput(alice1.Coin).AddInput(alice2.Coin).Finalize();
- round.SetPhase(Phase.TransactionSigning);
- using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, round);
-
- // Submit the signature for the second alice to the first alice's input.
- var alice2signedCoinJoin = round.Assert<SigningState>().CreateUnsignedTransaction();
- alice2signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), alice2.Coin);
-
- var req = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair(0, alice2signedCoinJoin.Inputs[0].WitScript) });
- await using ArenaRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
- var ex = await Assert.ThrowsAsync<WabiSabiProtocolException>(async () => await handler.SignTransactionAsync(req, CancellationToken.None));
- Assert.Equal(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, ex.ErrorCode);
- await arena.StopAsync(CancellationToken.None);
- }
}
}

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
@@ -120,7 +120,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Models
using Key key2 = new();
var alice1Coin = CreateCoin(script: key1.PubKey.WitHash.ScriptPubKey);
- var alice2Coin = CreateCoin(script: key1.PubKey.WitHash.ScriptPubKey);
+ var alice2Coin = CreateCoin(script: key2.PubKey.WitHash.ScriptPubKey);
var state = new ConstructionState(DefaultParameters).AddInput(alice1Coin).AddInput(alice2Coin);
@@ -138,20 +138,31 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Models
var alice2Tx = tx.Clone();
alice2Tx.Sign(key2.GetBitcoinSecret(Network.Main), alice2Coin);
+ var alice1SignedInput = alice1Tx.Inputs.Select((x, i) => (Input: x, Index: i)).Single(x => x.Input.HasWitScript());
+ var alice2SignedInput = alice2Tx.Inputs.Select((x, i) => (Input: x, Index: i)).Single(x => x.Input.HasWitScript());
+
+ var validSignatures = new[] { alice1SignedInput, alice2SignedInput };
+
+ var invalidSignatures = Enumerable.Concat(alice1Tx.Inputs, alice2Tx.Inputs)
+ .SelectMany(x => Enumerable.Range(0, 2), (input, idx) => (Input: input, Index: idx))
+ .Except(validSignatures);
+
// Only accept valid witnesses
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(1, alice1Tx.Inputs[0].WitScript));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(1, alice1Tx.Inputs[1].WitScript));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(0, alice1Tx.Inputs[1].WitScript));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(0, alice2Tx.Inputs[0].WitScript));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(0, alice2Tx.Inputs[1].WitScript));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(1, alice2Tx.Inputs[0].WitScript));
+ foreach (var invalidSignature in invalidSignatures)
+ {
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, () => noFeeTx.AddWitness(invalidSignature.Index, invalidSignature.Input.WitScript));
+ }
// Add Alice 1's signature
- var alice1Sig = noFeeTx.AddWitness(0, alice1Tx.Inputs[0].WitScript);
+ var alice1Sig = noFeeTx.AddWitness(alice1SignedInput.Index, alice1SignedInput.Input.WitScript);
Assert.False(alice1Sig.IsFullySigned);
+ // Add Alice 2's signature
+ var alice2Sig = alice1Sig.AddWitness(alice2SignedInput.Index, alice2SignedInput.Input.WitScript);
+ Assert.True(alice2Sig.IsFullySigned);
+
// Witness can only be accepted once per input
- ThrowsProtocolException(WabiSabiProtocolErrorCode.WitnessAlreadyProvided, () => alice1Sig.AddWitness(0, alice1Tx.Inputs[0].WitScript));
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.WitnessAlreadyProvided, () => alice1Sig.AddWitness(alice1SignedInput.Index, alice1SignedInput.Input.WitScript));
}
[Fact]
@@ -183,27 +194,25 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Models
var tx = generousFeeTx.CreateUnsignedTransaction();
- Assert.Collection(
- tx.Inputs.Select(x => x.PrevOut),
- outpoint => Assert.Equal(alice1Coin.Outpoint, outpoint),
- outpoint => Assert.Equal(alice2Coin.Outpoint, outpoint));
+ Assert.Contains(alice1Coin.Outpoint, tx.Inputs.Select(x => x.PrevOut));
+ Assert.Contains(alice2Coin.Outpoint, tx.Inputs.Select(x => x.PrevOut));
- Assert.Collection(
- tx.Outputs.Select(x => x.ScriptPubKey),
- scriptPubKey => Assert.Equal(alice1Coin.ScriptPubKey, scriptPubKey),
- scriptPubKey => Assert.Equal(alice2Coin.ScriptPubKey, scriptPubKey));
+ Assert.Contains(alice1Coin.ScriptPubKey, tx.Outputs.Select(x => x.ScriptPubKey));
+ Assert.Contains(alice2Coin.ScriptPubKey, tx.Outputs.Select(x => x.ScriptPubKey));
var alice1Tx = tx.Clone();
alice1Tx.Sign(key1.GetBitcoinSecret(Network.Main), alice1Coin);
- var alice1Sig = generousFeeTx.AddWitness(0, alice1Tx.Inputs[0].WitScript);
+ var alice1SignedInput = alice1Tx.Inputs.Select((x, i) => (Input: x, Index: i)).Single(x => x.Input.HasWitScript());
+ var alice1Sig = generousFeeTx.AddWitness(alice1SignedInput.Index, alice1SignedInput.Input.WitScript);
Assert.False(alice1Sig.IsFullySigned);
Assert.Equal(alice1Tx.ToString(), alice1Sig.CreateTransaction().ToString());
var alice2Tx = tx.Clone();
alice2Tx.Sign(key2.GetBitcoinSecret(Network.Main), alice2Coin);
- var alice2Sig = alice1Sig.AddWitness(1, alice2Tx.Inputs[1].WitScript);
+ var alice2SignedInput = alice2Tx.Inputs.Select((x, i) => (Input: x, Index: i)).Single(x => x.Input.HasWitScript());
+ var alice2Sig = alice1Sig.AddWitness(alice2SignedInput.Index, alice2SignedInput.Input.WitScript);
Assert.True(alice2Sig.IsFullySigned);
var signed = alice2Sig.CreateTransaction();

ADD
WalletWasabi/Helpers/ByteArrayComparer.cs
+using System.Collections.Generic;
+
+namespace WalletWasabi.Helpers
+{
+ public class ByteArrayComparer : IComparer<byte[]>
+ {
+ public static readonly ByteArrayComparer Comparer = new();
+
+ public int Compare(byte[]? x, byte[]? y)
+ {
+ static int InternalCompare(byte[] left, byte[] right)
+ {
+ var min = left.Length < right.Length ? left.Length : right.Length;
+ for (var i = 0; i < min; i++)
+ {
+ if (left[i] < right[i])
+ {
+ return -1;
+ }
+ if (left[i] > right[i])
+ {
+ return 1;
+ }
+ }
+ return left.Length.CompareTo(right.Length);
+ }
+
+ return (x, y) switch
+ {
+ (null, null) => 0,
+ (null, _) => 1,
+ (_ , null) => -1,
+ ({} left, {} right) => InternalCompare(left, right)
+ };
+ }
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
using NBitcoin;
using Newtonsoft.Json;
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using WalletWasabi.Helpers;
using WalletWasabi.WabiSabi.Backend.Models;
namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction
@@ -22,13 +24,17 @@ namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction
public bool IsFullySigned => Witnesses.Count == Inputs.Count;
[JsonIgnore]
- public IEnumerable<Coin> UnsignedInputs => Inputs.Where((_, i) => !IsInputSigned(i));
+ public IEnumerable<Coin> UnsignedInputs => GetSortedInputs().Where((_, i) => !IsInputSigned(i));
public bool IsInputSigned(int index) => Witnesses.ContainsKey(index);
public bool IsInputSigned(OutPoint prevout) => IsInputSigned(GetInputIndex(prevout));
- public int GetInputIndex(OutPoint prevout) => Inputs.ToList().FindIndex(coin => coin.Outpoint == prevout); // this is inefficient but is only used in tests, see also dotnet/runtime#45366
+ public int GetInputIndex(OutPoint prevout) =>
+ GetSortedInputs()
+ .Select((x,i) => (OutPoint: x.Outpoint, Index: i))
+ .First(x => x.OutPoint == prevout)
+ .Index; // this is inefficient but is only used in tests, see also dotnet/runtime#45366
public SigningState AddWitness(int index, WitScript witness)
{
@@ -48,7 +54,7 @@ namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction
IndexedTxIn currentIndexedInput = cjCopy.Inputs.AsIndexedInputs().Skip(index).First();
// 4. Find the corresponding registered input.
- Coin registeredCoin = Inputs[index];
+ Coin registeredCoin = Inputs.Single(x => x.Outpoint == currentIndexedInput.PrevOut);
// 5. Verify if currentIndexedInput is correctly signed, if not, return the specific error.
if (!currentIndexedInput.VerifyScript(registeredCoin, out ScriptError error))
@@ -63,16 +69,20 @@ namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction
{
var tx = Parameters.CreateTransaction();
- foreach (var coin in Inputs)
+ foreach (var coin in GetSortedInputs())
{
// implied:
// nSequence = FINAL
tx.Inputs.Add(coin.Outpoint);
}
- foreach (var txout in Outputs)
+ foreach (var txout in Outputs
+ .GroupBy(x => x.ScriptPubKey)
+ .Select(x => new TxOut(x.Sum(y => y.Value), x.Key))
+ .OrderByDescending(x => x.Value)
+ .ThenBy(x => x.ScriptPubKey.ToBytes(true), ByteArrayComparer.Comparer))
{
- tx.Outputs.AddWithOptimize(txout.Value, txout.ScriptPubKey);
+ tx.Outputs.Add(txout);
}
return tx;
@@ -89,5 +99,10 @@ namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction
return tx;
}
+
+ private IEnumerable<Coin> GetSortedInputs() =>
+ Inputs
+ .OrderByDescending(x => x.Amount)
+ .ThenBy(x => x.Outpoint.ToBytes(), ByteArrayComparer.Comparer);
}
}

Sort inputs and outputs