Move ownership proof verification to ConstructionState
MODIFY
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
@@ -37,6 +37,18 @@ public static class WabiSabiFactory
return new(new OutPoint(Hashes.DoubleSHA256(key.PubKey.ToBytes()), 0), new TxOut(amount, key.PubKey.WitHash.ScriptPubKey));
}
+ public static Tuple<Coin, OwnershipProof> CreateCoinWithOwnershipProof(Key? key = null, Money? amount = null, uint256? roundId = null)
+ {
+ key = key ?? new();
+ var coin = WabiSabiFactory.CreateCoin(key, amount);
+ roundId ??= uint256.One;
+ var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, roundId);
+ return new Tuple<Coin, OwnershipProof>(coin, ownershipProof);
+ }
+
+ public static CoinJoinInputCommitmentData CreateCommitmentData(uint256? RoundId = null)
+ => new CoinJoinInputCommitmentData("wasabiwallet.io", RoundId ?? uint256.One);
+
public static OwnershipProof CreateOwnershipProof(Key key, uint256? roundHash = null)
=> OwnershipProof.GenerateCoinJoinInputProof(
key,
@@ -112,7 +124,7 @@ public static class WabiSabiFactory
=> new(coin, ownershipProof, round, Guid.NewGuid(), false) { Deadline = DateTimeOffset.UtcNow + TimeSpan.FromHours(1) };
public static Alice CreateAlice(Key key, Money amount, Round round)
- => CreateAlice(CreateCoin(key, amount), CreateOwnershipProof(key), round);
+ => CreateAlice(CreateCoin(key, amount), CreateOwnershipProof(key, round.Id), round);
public static Alice CreateAlice(Money amount, Round round)
{

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepOutputRegistrationTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepOutputRegistrationTests.cs
@@ -141,7 +141,7 @@ public class StepOutputRegistrationTests
var txParams = round.Parameters;
var extraAlice = WabiSabiFactory.CreateAlice(round.Parameters.MiningFeeRate.GetFee(Constants.P2wpkhInputVirtualSize) + txParams.AllowedOutputAmounts.Min - new Money(1L), round);
round.Alices.Add(extraAlice);
- round.CoinjoinState = round.Assert<ConstructionState>().AddInput(extraAlice.Coin);
+ round.CoinjoinState = round.Assert<ConstructionState>().AddInput(extraAlice.Coin, extraAlice.OwnershipProof, WabiSabiFactory.CreateCommitmentData(round.Id));
await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
Assert.Equal(Phase.TransactionSigning, round.Phase);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepTransactionSigningTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepTransactionSigningTests.cs
@@ -201,7 +201,7 @@ public class StepTransactionSigningTests
var alice3 = WabiSabiFactory.CreateAlice(round);
alice3.ConfirmedConnection = true;
round.Alices.Add(alice3);
- round.CoinjoinState = round.Assert<ConstructionState>().AddInput(alice3.Coin);
+ round.CoinjoinState = round.Assert<ConstructionState>().AddInput(alice3.Coin, alice3.OwnershipProof, WabiSabiFactory.CreateCommitmentData(round.Id));
await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
Assert.Equal(Phase.TransactionSigning, round.Phase);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PostRequests/SignTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PostRequests/SignTransactionTests.cs
@@ -22,7 +22,7 @@ public class SignTransactionTests
using Key key = new();
Alice alice = WabiSabiFactory.CreateAlice(key: key, round: round);
round.Alices.Add(alice);
- round.CoinjoinState = round.AddInput(alice.Coin).Finalize();
+ round.CoinjoinState = round.AddInput(alice.Coin, alice.OwnershipProof, WabiSabiFactory.CreateCommitmentData(round.Id)).Finalize();
round.SetPhase(Phase.TransactionSigning);
using Arena arena = await ArenaBuilder.From(cfg).CreateAndStartAsync(round);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/Rounds/MultipartyTransactionStateTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/Rounds/MultipartyTransactionStateTests.cs
@@ -17,13 +17,17 @@ public class MultipartyTransactionStateTests
var cfg = new WabiSabiConfig();
var round = WabiSabiFactory.CreateRound(cfg);
- Coin coin1, coin2, coin3;
+ var commitmentData = WabiSabiFactory.CreateCommitmentData(round.Id);
+
+ (var coin1, var ownershipProof1) = WabiSabiFactory.CreateCoinWithOwnershipProof(roundId: round.Id);
+ (var coin2, var ownershipProof2) = WabiSabiFactory.CreateCoinWithOwnershipProof(roundId: round.Id);
+ (var coin3, var ownershipProof3) = WabiSabiFactory.CreateCoinWithOwnershipProof(roundId: round.Id);
// Three events / three states
var state0 = round.Assert<ConstructionState>();
- var state1 = state0.AddInput(coin1 = WabiSabiFactory.CreateCoin());
- var state2 = state1.AddInput(coin2 = WabiSabiFactory.CreateCoin());
- var state3 = state2.AddInput(coin3 = WabiSabiFactory.CreateCoin());
+ var state1 = state0.AddInput(coin1, ownershipProof1, commitmentData);
+ var state2 = state1.AddInput(coin2, ownershipProof2, commitmentData);
+ var state3 = state2.AddInput(coin3, ownershipProof3, commitmentData);
// Unknown state. Assumes full state is required
var diffd30 = state3.GetStateFrom(-1);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
@@ -212,7 +212,7 @@ public class ArenaClientTests
var coins = destinationProvider.GetNextDestinations(2)
.Select(dest => (
Coin: new Coin(BitcoinFactory.CreateOutPoint(), new TxOut(Money.Coins(1.0m), dest)),
- OwnershipProof: keyChain.GetOwnershipProof(dest, new CoinJoinInputCommitmentData("test", uint256.One))))
+ OwnershipProof: keyChain.GetOwnershipProof(dest, WabiSabiFactory.CreateCommitmentData(round.Id))))
.ToArray();
Alice alice1 = WabiSabiFactory.CreateAlice(coins[0].Coin, coins[0].OwnershipProof, round: round);
@@ -238,6 +238,7 @@ public class ArenaClientTests
round.SetPhase(Phase.TransactionSigning);
var emptyState = round.Assert<ConstructionState>();
+ var commitmentData = WabiSabiFactory.CreateCommitmentData(round.Id);
// We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it
var finalizedEmptyState = new SigningState(round.Parameters, emptyState.Events);
@@ -246,14 +247,14 @@ public class ArenaClientTests
await Assert.ThrowsAsync<ArgumentException>(async () =>
await apiClient.SignTransactionAsync(round.Id, alice1.Coin, coins[0].OwnershipProof, keyChain, finalizedEmptyState.CreateUnsignedTransaction(), CancellationToken.None));
- var oneInput = emptyState.AddInput(alice1.Coin).Finalize();
+ var oneInput = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).Finalize();
round.CoinjoinState = oneInput;
// Trying to sign coins those are not in the coinjoin.
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await apiClient.SignTransactionAsync(round.Id, alice2.Coin, coins[1].OwnershipProof, keyChain, oneInput.CreateUnsignedTransaction(), CancellationToken.None));
- var twoInputs = emptyState.AddInput(alice1.Coin).AddInput(alice2.Coin).Finalize();
+ var twoInputs = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).AddInput(alice2.Coin, alice2.OwnershipProof, commitmentData).Finalize();
round.CoinjoinState = twoInputs;
Assert.False(round.Assert<SigningState>().IsFullySigned);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/InputRegistrationRequestTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/InputRegistrationRequestTests.cs
@@ -123,5 +123,5 @@ public class InputRegistrationRequestTests
=> OwnershipProof.GenerateCoinJoinInputProof(
key,
new OwnershipIdentifier(Key.Parse("5KbdaBwc9Eit2LrmDp1WfZd815StNstwHanbRrPpGGN6wWJKyHe", Network.Main), key.PubKey.WitHash.ScriptPubKey),
- new CoinJoinInputCommitmentData("wasabiwallet.io", roundHash));
+ WabiSabiFactory.CreateCommitmentData(roundHash));
}

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/MultipartyTransactionTests.cs
using NBitcoin;
using System.Collections.Immutable;
+using WalletWasabi.Crypto;
using System.Linq;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Models;
@@ -22,6 +23,8 @@ public class MultipartyTransactionTests
MaxSuggestedAmountBase = Money.Coins(Constants.MaximumNumberOfBitcoins)
}) with { MiningFeeRate = new FeeRate(0m)};
+ private static CoinJoinInputCommitmentData commitmentData = WabiSabiFactory.CreateCommitmentData();
+
private static void ThrowsProtocolException(WabiSabiProtocolErrorCode expectedError, Action action) =>
Assert.Equal(expectedError, Assert.Throws<WabiSabiProtocolException>(action).ErrorCode);
@@ -31,15 +34,15 @@ public class MultipartyTransactionTests
using Key key1 = new();
using Key key2 = new();
- var alice1Coin = WabiSabiFactory.CreateCoin(key1);
- var alice2Coin = WabiSabiFactory.CreateCoin(key2);
+ (var alice1Coin, var alice1OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key1);
+ (var alice2Coin, var alice2OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key2);
var state = new ConstructionState(DefaultParameters);
Assert.Empty(state.Inputs);
Assert.Empty(state.Outputs);
- var oneInput = state.AddInput(alice1Coin);
+ var oneInput = state.AddInput(alice1Coin, alice1OwnershipProof, commitmentData);
Assert.Single(oneInput.Inputs);
Assert.Empty(oneInput.Outputs);
@@ -48,14 +51,14 @@ public class MultipartyTransactionTests
Assert.Empty(state.Inputs);
Assert.Empty(state.Outputs);
- var differentInput = state.AddInput(alice2Coin);
+ var differentInput = state.AddInput(alice2Coin, alice2OwnershipProof, commitmentData);
Assert.Single(differentInput.Inputs);
Assert.Empty(differentInput.Outputs);
Assert.NotEqual(oneInput.Inputs, differentInput.Inputs);
Assert.Equal(oneInput.Outputs, differentInput.Outputs);
- var twoInputs = oneInput.AddInput(alice2Coin);
+ var twoInputs = oneInput.AddInput(alice2Coin, alice2OwnershipProof, commitmentData);
Assert.Equal(2, twoInputs.Inputs.Count());
Assert.Empty(twoInputs.Outputs);
@@ -107,9 +110,9 @@ public class MultipartyTransactionTests
[Fact]
public void AddWithOptimize()
{
- var coin = WabiSabiFactory.CreateCoin();
+ (var coin, var ownershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof();
- var state = new ConstructionState(DefaultParameters).AddInput(coin);
+ var state = new ConstructionState(DefaultParameters).AddInput(coin, ownershipProof, commitmentData);
var script = BitcoinFactory.CreateScript();
var bob = new TxOut(coin.Amount / 2, script);
@@ -125,13 +128,14 @@ public class MultipartyTransactionTests
[Fact]
public void WitnessValidation()
{
+
using Key key1 = new();
using Key key2 = new();
- var alice1Coin = WabiSabiFactory.CreateCoin(key1);
- var alice2Coin = WabiSabiFactory.CreateCoin(key2);
+ (var alice1Coin, var alice1OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key1);
+ (var alice2Coin, var alice2OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key2);
- var state = new ConstructionState(DefaultParameters).AddInput(alice1Coin).AddInput(alice2Coin);
+ var state = new ConstructionState(DefaultParameters).AddInput(alice1Coin, alice1OwnershipProof, commitmentData).AddInput(alice2Coin, alice2OwnershipProof, commitmentData);
// address reuse bad
var bob1 = new TxOut(Money.Coins(1), alice1Coin.ScriptPubKey);
@@ -182,12 +186,12 @@ public class MultipartyTransactionTests
using Key key1 = new();
using Key key2 = new();
- var alice1Coin = WabiSabiFactory.CreateCoin(key1);
- var alice2Coin = WabiSabiFactory.CreateCoin(key2);
+ (var alice1Coin, var alice1OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key1);
+ (var alice2Coin, var alice2OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(key2);
var state = new ConstructionState(DefaultParameters with { MiningFeeRate = feeRate })
- .AddInput(alice1Coin)
- .AddInput(alice2Coin);
+ .AddInput(alice1Coin, alice1OwnershipProof, commitmentData)
+ .AddInput(alice2Coin, alice2OwnershipProof, commitmentData);
var bob1 = new TxOut(Money.Coins(1), alice1Coin.ScriptPubKey);
var withOutput = state.AddOutput(bob1);
@@ -238,9 +242,9 @@ public class MultipartyTransactionTests
[Fact]
public void NoDuplicateInputs()
{
- var coin = WabiSabiFactory.CreateCoin();
- var state = new ConstructionState(DefaultParameters).AddInput(coin);
- ThrowsProtocolException(WabiSabiProtocolErrorCode.NonUniqueInputs, () => state.AddInput(coin));
+ (var coin, var ownershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof();
+ var state = new ConstructionState(DefaultParameters).AddInput(coin, ownershipProof, commitmentData);
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.NonUniqueInputs, () => state.AddInput(coin, ownershipProof, commitmentData));
Assert.Single(state.Inputs);
}
@@ -250,31 +254,31 @@ public class MultipartyTransactionTests
public void OnlyAllowedInputTypes()
{
var legacyOnly = new ConstructionState(DefaultParameters with { AllowedInputTypes = ImmutableSortedSet.Create<ScriptType>(ScriptType.P2PKH) });
- var coin = WabiSabiFactory.CreateCoin();
- ThrowsProtocolException(WabiSabiProtocolErrorCode.ScriptNotAllowed, () => legacyOnly.AddInput(coin));
+ (var coin, var ownershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof();
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.ScriptNotAllowed, () => legacyOnly.AddInput(coin, ownershipProof, commitmentData));
}
[Fact]
public void InputAmountRanges()
{
- var coin = WabiSabiFactory.CreateCoin();
+ (var coin, var ownershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof();
var exact = new ConstructionState(DefaultParameters with { AllowedInputAmounts = new MoneyRange(coin.Amount, coin.Amount) });
var above = new ConstructionState(DefaultParameters with { AllowedInputAmounts = new MoneyRange(2 * coin.Amount, 3 * coin.Amount) });
var below = new ConstructionState(DefaultParameters with { AllowedInputAmounts = new MoneyRange(coin.Amount - Money.Coins(0.001m), coin.Amount - Money.Coins(0.0001m)) });
- ThrowsProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds, () => above.AddInput(coin));
- ThrowsProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds, () => below.AddInput(coin));
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds, () => above.AddInput(coin, ownershipProof, commitmentData));
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds, () => below.AddInput(coin, ownershipProof, commitmentData));
// Allowed range is inclusive:
- Assert.Equal(coin.Amount, Assert.Single(exact.AddInput(coin).Inputs).Amount);
+ Assert.Equal(coin.Amount, Assert.Single(exact.AddInput(coin, ownershipProof, commitmentData).Inputs).Amount);
}
[Fact]
public void UneconomicalInputs()
{
- var alice1Coin = WabiSabiFactory.CreateCoin(amount: new Money(1000L));
- var alice2Coin = WabiSabiFactory.CreateCoin(amount: new Money(1001L));
+ (var alice1Coin, var alice1OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(amount: new Money(1000L));
+ (var alice2Coin, var alice2OwnershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(amount: new Money(2000L));
// requires 1k sats per input in sat/vKB
var inputVsize = alice1Coin.ScriptPubKey.EstimateInputVsize();
@@ -283,9 +287,9 @@ public class MultipartyTransactionTests
var state = new ConstructionState(DefaultParameters with { MiningFeeRate = feeRate });
- ThrowsProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput, () => state.AddInput(alice1Coin));
+ ThrowsProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput, () => state.AddInput(alice1Coin, alice1OwnershipProof, commitmentData));
- Assert.Equal(alice2Coin.Amount, Assert.Single(state.AddInput(alice2Coin).Inputs).Amount);
+ Assert.Equal(alice2Coin.Amount, Assert.Single(state.AddInput(alice2Coin, alice2OwnershipProof, commitmentData).Inputs).Amount);
}
[Fact]

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/SerializationTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/SerializationTests.cs
@@ -139,8 +139,9 @@ public class SerializationTests
AssertSerialization(RoundState.FromRound(round));
var state = round.Assert<ConstructionState>();
- state = state.AddInput(WabiSabiFactory.CreateCoin());
- round.CoinjoinState = new SigningState(round.Parameters, state.Events);
+ (var coin, var ownershipProof) = WabiSabiFactory.CreateCoinWithOwnershipProof(roundId: round.Id);
+ state = state.AddInput(coin, ownershipProof, WabiSabiFactory.CreateCommitmentData(round.Id));
+ round.CoinjoinState = new SigningState(state.Parameters, state.Events);
AssertSerialization(RoundState.FromRound(round));
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
@@ -58,16 +58,12 @@ public partial class Arena : IWabiSabiApiRequestHandler
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
}
+ var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData(Config.CoordinatorIdentifier, request.RoundId);
+
// Compute but don't commit updated coinjoin to round state, it will
// be re-calculated on input confirmation. This is computed in here
// for validation purposes.
- _ = round.Assert<ConstructionState>().AddInput(coin);
-
- var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData(Config.CoordinatorIdentifier, round.Id);
- if (!OwnershipProof.VerifyCoinJoinInputProof(request.OwnershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
- {
- throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof);
- }
+ _ = round.Assert<ConstructionState>().AddInput(coin, request.OwnershipProof, coinJoinInputCommitmentData);
// Generate a new GUID with the secure random source, to be sure
// that it is not guessable (Guid.NewGuid() documentation does
@@ -236,7 +232,8 @@ public partial class Arena : IWabiSabiApiRequestHandler
await vsizeRealCredentialTask.ConfigureAwait(false));
// Update the coinjoin state, adding the confirmed input.
- round.CoinjoinState = round.Assert<ConstructionState>().AddInput(alice.Coin);
+ var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData(Config.CoordinatorIdentifier, request.RoundId);
+ round.CoinjoinState = round.Assert<ConstructionState>().AddInput(alice.Coin, alice.OwnershipProof, coinJoinInputCommitmentData);
alice.ConfirmedConnection = true;
return response;

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
@@ -127,8 +127,8 @@ public class Round
return InputRegistrationTimeFrame.HasExpired;
}
- public ConstructionState AddInput(Coin coin)
- => Assert<ConstructionState>().AddInput(coin);
+ public ConstructionState AddInput(Coin coin, OwnershipProof ownershipProof, CoinJoinInputCommitmentData coinJoinInputCommitmentData)
+ => Assert<ConstructionState>().AddInput(coin, ownershipProof, coinJoinInputCommitmentData);
public ConstructionState AddOutput(TxOut output)
=> Assert<ConstructionState>().AddOutput(output);

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/ConstructionState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/ConstructionState.cs
using NBitcoin;
using System.Linq;
+using WalletWasabi.Crypto;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.Rounds;
@@ -13,8 +14,7 @@ public record ConstructionState : MultipartyTransactionState
{
}
- // TODO ownership proofs and spend status also in scope
- public ConstructionState AddInput(Coin coin)
+ public ConstructionState AddInput(Coin coin, OwnershipProof ownershipProof, CoinJoinInputCommitmentData coinJoinInputCommitmentData)
{
var prevout = coin.TxOut;
@@ -61,6 +61,11 @@ public record ConstructionState : MultipartyTransactionState
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NonUniqueInputs);
}
+ if (!OwnershipProof.VerifyCoinJoinInputProof(ownershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof);
+ }
+
return this with { Events = Events.Add(new InputAdded(coin)) };
}

Move ownership proof verification to ConstructionState