Use coinjoin events instead of inputs and outputs
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
@@ -233,7 +233,7 @@ public class ArenaClientTests
var emptyState = round.Assert<ConstructionState>();
// We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it
- var finalizedEmptyState = new SigningState(emptyState.Parameters, emptyState.Inputs, emptyState.Outputs);
+ var finalizedEmptyState = new SigningState(emptyState.Parameters, emptyState.Events);
// No inputs in the coinjoin.
await Assert.ThrowsAsync<ArgumentException>(async () =>

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
@@ -49,14 +49,14 @@ public class MultipartyTransactionTests
var twoInputs = oneInput.AddInput(alice2Coin);
- Assert.Equal(2, twoInputs.Inputs.Count);
+ Assert.Equal(2, twoInputs.Inputs.Count());
Assert.Empty(twoInputs.Outputs);
// address reuse bad
var bob1 = new TxOut(Money.Coins(1), alice1Coin.ScriptPubKey);
var withOutput = twoInputs.AddOutput(bob1);
- Assert.Equal(2, withOutput.Inputs.Count);
+ Assert.Equal(2, withOutput.Inputs.Count());
Assert.Single(withOutput.Outputs);
var bob2 = new TxOut(Money.Coins(1), alice2Coin.ScriptPubKey);
@@ -184,7 +184,7 @@ public class MultipartyTransactionTests
var bob1 = new TxOut(Money.Coins(1), alice1Coin.ScriptPubKey);
var withOutput = state.AddOutput(bob1);
- Assert.Equal(2, withOutput.Inputs.Count);
+ Assert.Equal(2, withOutput.Inputs.Count());
Assert.Single(withOutput.Outputs);
var bob2 = new TxOut(Money.Coins(1), alice2Coin.ScriptPubKey);

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -176,8 +176,8 @@ public partial class Arena : PeriodicRunner
{
var coinjoin = round.Assert<ConstructionState>();
- round.LogInfo($"{coinjoin.Inputs.Count} inputs were added.");
- round.LogInfo($"{coinjoin.Outputs.Count} outputs were added.");
+ round.LogInfo($"{coinjoin.Inputs.Count()} inputs were added.");
+ round.LogInfo($"{coinjoin.Outputs.Count()} outputs were added.");
coinjoin = AddCoordinationFee(round, coinjoin);

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/ConstructionState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/ConstructionState.cs
@@ -60,7 +60,7 @@ public record ConstructionState : MultipartyTransactionState
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NonUniqueInputs);
}
- return this with { Inputs = Inputs.Add(coin), PreviousStates = PreviousStates.Add(this) };
+ return this with { Events = Events.Add(new InputAdded(coin)) };
}
public ConstructionState AddOutput(TxOut output)
@@ -93,7 +93,7 @@ public record ConstructionState : MultipartyTransactionState
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.ScriptNotAllowed);
}
- return this with { Outputs = Outputs.Add(output), PreviousStates = PreviousStates.Add(this) };
+ return this with { Events = Events.Add(new OutputAdded(output)) };
}
public SigningState Finalize()
@@ -108,6 +108,6 @@ public record ConstructionState : MultipartyTransactionState
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InsufficientFees, $"Effective fee rate {EffectiveFeeRate} is less than required {Parameters.FeeRate}.");
}
- return new SigningState(Parameters, Inputs, Outputs);
+ return new SigningState(Parameters, Events);
}
}

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
using NBitcoin;
+using Newtonsoft.Json;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction;
+public interface IEvent{};
+public record InputAdded (Coin Coin) : IEvent;
+public record OutputAdded (TxOut Output) : IEvent;
+
public abstract record MultipartyTransactionState
{
protected MultipartyTransactionState(MultipartyTransactionParameters parameters)
@@ -14,8 +20,10 @@ public abstract record MultipartyTransactionState
public MultipartyTransactionParameters Parameters { get; }
- public ImmutableList<Coin> Inputs { get; init; } = ImmutableList<Coin>.Empty;
- public ImmutableList<TxOut> Outputs { get; init; } = ImmutableList<TxOut>.Empty;
+ [JsonIgnore]
+ public IEnumerable<Coin> Inputs => Events.OfType<InputAdded>().Select(x => x.Coin);
+ [JsonIgnore]
+ public IEnumerable<TxOut> Outputs => Events.OfType<OutputAdded>().Select(x => x.Output);
public Money Balance => Inputs.Sum(x => x.Amount) - Outputs.Sum(x => x.Value);
public int EstimatedInputsVsize => Inputs.Sum(x => x.TxOut.ScriptPubKey.EstimateInputVsize());
@@ -30,30 +38,20 @@ public abstract record MultipartyTransactionState
// including the shared overhead
public FeeRate EffectiveFeeRate => new(Balance, EstimatedInputsVsize + OutputsVsize);
- public ImmutableList<MultipartyTransactionState> PreviousStates { get; init; } = ImmutableList<MultipartyTransactionState>.Empty;
-
- public MultipartyTransactionState GetConstructionStateSince(int order)
- {
- var state = PreviousStates.IsEmpty || order >= PreviousStates.Count
- ? this
- : PreviousStates[order < 0 ? 0 : order];
+ public ImmutableList<IEvent> Events { get; init; } = ImmutableList<IEvent>.Empty;
- return this with {
- Inputs = Inputs.GetRange(state.Inputs.Count, Inputs.Count - state.Inputs.Count),
- Outputs = Outputs.GetRange(state.Outputs.Count, Outputs.Count - state.Outputs.Count)
+ public MultipartyTransactionState GetConstructionStateSince(int order) =>
+ this with {
+ Events = Events.Skip(order).ToImmutableList()
};
- }
public MultipartyTransactionState Merge(MultipartyTransactionState diff) =>
this with {
- Inputs = Inputs.AddRange(diff.Inputs),
- Outputs = Outputs.AddRange(diff.Outputs),
- PreviousStates = diff.PreviousStates,
+ Events = Events.AddRange(diff.Events)
};
public MultipartyTransactionState MergeBack(MultipartyTransactionState origin) =>
this with {
- Inputs = origin.Inputs.AddRange(Inputs),
- Outputs = origin.Outputs.AddRange(Outputs),
+ Events = origin.Events.AddRange(Events)
};
}

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
@@ -10,34 +10,40 @@ namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction;
public record SigningState : MultipartyTransactionState
{
- public SigningState(MultipartyTransactionParameters parameters, IEnumerable<Coin> inputs, IEnumerable<TxOut> outputs)
+ public SigningState(MultipartyTransactionParameters parameters, IEnumerable<IEvent> events)
: base(parameters)
{
- Inputs = inputs
+ Events = events.ToImmutableList();
+ SortedInputs = Inputs
.OrderByDescending(x => x.Amount)
.ThenBy(x => x.Outpoint.ToBytes(), ByteArrayComparer.Comparer)
- .ToImmutableList();
+ .ToList();
- Outputs = outputs
+ SortedOutputs = 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)
- .ToImmutableList();
+ .ToList();
}
public ImmutableDictionary<int, WitScript> Witnesses { get; init; } = ImmutableDictionary<int, WitScript>.Empty;
- public bool IsFullySigned => Witnesses.Count == Inputs.Count;
+ public bool IsFullySigned => Witnesses.Count == SortedInputs.Count;
[JsonIgnore]
- public IEnumerable<Coin> UnsignedInputs => Inputs.Where((_, i) => !IsInputSigned(i));
+ public IEnumerable<Coin> UnsignedInputs => SortedInputs.Where((_, i) => !IsInputSigned(i));
+
+ [JsonIgnore]
+ public List<Coin> SortedInputs { get; }
+ [JsonIgnore]
+ public List<TxOut> SortedOutputs { get; }
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) => SortedInputs.FindIndex(coin => coin.Outpoint == prevout); // this is inefficient but is only used in tests, see also dotnet/runtime#45366
public SigningState AddWitness(int index, WitScript witness)
{
@@ -57,7 +63,7 @@ public record SigningState : MultipartyTransactionState
IndexedTxIn currentIndexedInput = cjCopy.Inputs.AsIndexedInputs().Skip(index).First();
// 4. Find the corresponding registered input.
- Coin registeredCoin = Inputs[index];
+ Coin registeredCoin = SortedInputs[index];
// 5. Verify if currentIndexedInput is correctly signed, if not, return the specific error.
if (!currentIndexedInput.VerifyScript(registeredCoin, out ScriptError error))
@@ -72,14 +78,14 @@ public record SigningState : MultipartyTransactionState
{
var tx = Parameters.CreateTransaction();
- foreach (var coin in Inputs)
+ foreach (var coin in SortedInputs)
{
// implied:
// nSequence = FINAL
tx.Inputs.Add(coin.Outpoint);
}
- foreach (var txout in Outputs)
+ foreach (var txout in SortedOutputs)
{
tx.Outputs.Add(txout);
}

Use coinjoin events instead of inputs and outputs