Keep track of each individual tx state
ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/Rounds/MultipartyTransactionStateTests.cs
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using NBitcoin;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend.Rounds;
+
+public class MultipartyTransactionStateTests
+{
+ [Fact]
+ public void CanGetDifferentialStateTest()
+ {
+ var cfg = new WabiSabiConfig();
+ var round = WabiSabiFactory.CreateRound(cfg);
+
+ static Coin CreateCoin()
+ {
+ using var key = new Key();
+ return WabiSabiFactory.CreateCoin(key);
+ }
+
+ Coin coin1, coin2, coin3;
+
+ // Three events / three states
+ var state0 = round.Assert<ConstructionState>();
+ var state1 = state0.AddInput(coin1 = CreateCoin());
+ var state2 = state1.AddInput(coin2 = CreateCoin());
+ var state3 = state2.AddInput(coin3 = CreateCoin());
+
+ // Unknown state. Assumes full state is required
+ var diffd30 = state3.GetConstructionStateSince(-1);
+ Assert.Equal(state3.Inputs, diffd30.Inputs);
+ Assert.Equal(state3.Outputs, diffd30.Outputs);
+
+ // Only one event is missing
+ var diffd32 = state3.GetConstructionStateSince(2);
+ var input = Assert.Single(diffd32.Inputs);
+ Assert.Equal(coin3.Outpoint, input.Outpoint);
+
+ // two events are missing
+ var diffd31 = state3.GetConstructionStateSince(1);
+ Assert.Collection(diffd31.Inputs,
+ x => Assert.Equal(coin2.Outpoint, x.Outpoint),
+ x => Assert.Equal(coin3.Outpoint, x.Outpoint));
+
+ // No event is missing (already updated)
+ var diffd33 = state3.GetConstructionStateSince(3);
+ Assert.Empty(diffd33.Inputs);
+ Assert.Empty(diffd33.Outputs);
+
+ // Merge initial state0 with full diff. Expected to get state3
+ var merged03 = state0.Merge(diffd30);
+ Assert.Equal(state3.Inputs, merged03.Inputs);
+ Assert.Equal(state3.Outputs, merged03.Outputs);
+
+ // Merge state1 with diff between 1 and 3. Expected to get state3
+ var merged13 = state1.Merge(diffd31);
+ Assert.True(merged13.Inputs.SequenceEqual(state3.Inputs));
+
+ var diff00 = state0.GetConstructionStateSince(0);
+ var diff10 = state1.GetConstructionStateSince(0);
+ var diff21 = state2.GetConstructionStateSince(1);
+ var diff32 = state3.GetConstructionStateSince(2);
+ var clientState1 = state1;
+ var clientState3 = state3.GetConstructionStateSince(1).MergeBack(clientState1);
+ Assert.Equal(state3.Inputs, clientState3.Inputs);
+ Assert.True(clientState3.Inputs.SequenceEqual(state3.Inputs));
+ }
+}
\ No newline at end of file

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) };
+ return this with { Inputs = Inputs.Add(coin), PreviousStates = PreviousStates.Add(this) };
}
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) };
+ return this with { Outputs = Outputs.Add(output), PreviousStates = PreviousStates.Add(this) };
}
public SigningState Finalize()

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
using NBitcoin;
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace WalletWasabi.WabiSabi.Models.MultipartyTransaction;
@@ -28,4 +29,31 @@ public abstract record MultipartyTransactionState
// to do for now, but in the future EstimatedVsize should be used
// 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];
+
+ 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 Merge(MultipartyTransactionState diff) =>
+ this with {
+ Inputs = Inputs.AddRange(diff.Inputs),
+ Outputs = Outputs.AddRange(diff.Outputs),
+ PreviousStates = diff.PreviousStates,
+ };
+
+ public MultipartyTransactionState MergeBack(MultipartyTransactionState origin) =>
+ this with {
+ Inputs = origin.Inputs.AddRange(Inputs),
+ Outputs = origin.Outputs.AddRange(Outputs),
+ };
}

MODIFY
WalletWasabi/WabiSabi/Models/RoundState.cs
WalletWasabi/WabiSabi/Models/RoundState.cs
@@ -31,7 +31,7 @@ public record RoundState(
public DateTimeOffset InputRegistrationEnd => InputRegistrationStart + InputRegistrationTimeout;
- public static RoundState FromRound(Round round) =>
+ public static RoundState FromRound(Round round, int order = -1) =>
new(
round is BlameRound blameRound ? blameRound.BlameOf.Id : uint256.Zero,
round.AmountCredentialIssuerParameters,
@@ -48,7 +48,7 @@ public record RoundState(
round.MaxAmountCredentialValue,
round.MaxVsizeCredentialValue,
round.MaxVsizeAllocationPerAlice,
- round.CoinjoinState);
+ round.CoinjoinState.GetConstructionStateSince(order));
public TState Assert<TState>() where TState : MultipartyTransactionState =>
CoinjoinState switch

Keep track of each individual tx state