Implement transaction signing
MODIFY
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
@@ -102,10 +102,17 @@ namespace WalletWasabi.Tests.Helpers
return new uint256(bytes);
}
- public static Script CreateScript()
+ public static Script CreateScript(Key? key = null)
+ {
+ if (key is null)
{
using Key k = new();
return k.PubKey.WitHash.ScriptPubKey;
}
+ else
+ {
+ return key.PubKey.WitHash.ScriptPubKey;
+ }
+ }
}
}

MODIFY
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
@@ -65,16 +65,16 @@ namespace WalletWasabi.Tests.Helpers
new FeeRate(100m),
new InsecureRandom());
- public static Alice CreateAlice(InputRoundSignaturePair inputSigPairs, Money? value = null) => CreateAlice(new[] { inputSigPairs }, value);
+ public static Alice CreateAlice(InputRoundSignaturePair inputSigPairs, Key? key = null, Money? value = null) => CreateAlice(new[] { inputSigPairs }, key, value);
- public static Alice CreateAlice(IEnumerable<InputRoundSignaturePair>? inputSigPairs = null, Money? value = null)
+ public static Alice CreateAlice(IEnumerable<InputRoundSignaturePair>? inputSigPairs = null, Key? key = null, Money? value = null)
{
var pairs = inputSigPairs ?? CreateInputRoundSignaturePairs(1);
var myDic = new Dictionary<Coin, byte[]>();
foreach (var pair in pairs)
{
- var coin = new Coin(pair.Input, new TxOut(value ?? Money.Coins(1), BitcoinFactory.CreateScript()));
+ var coin = new Coin(pair.Input, new TxOut(value ?? Money.Coins(1), BitcoinFactory.CreateScript(key)));
myDic.Add(coin, pair.RoundSignature);
}
return new Alice(myDic);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterOutputTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterOutputTests.cs
@@ -17,6 +17,26 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
{
public class RegisterOutputTests
{
+ [Fact]
+ public async Task SuccessAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ round.Phase = Phase.OutputRegistration;
+ round.Alices.Add(WabiSabiFactory.CreateAlice());
+ arena.OnTryGetRound = _ => round;
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+
+ var req = WabiSabiFactory.CreateOutputRegistrationRequest(round);
+ var resp = handler.RegisterOutput(req);
+ Assert.NotEmpty(round.Bobs);
+ Assert.NotNull(resp);
+ Assert.NotNull(resp.AmountCredentials);
+ Assert.NotNull(resp.WeightCredentials);
+ Assert.NotNull(resp.UnsignedTransactionSecret);
+ }
+
[Fact]
public async Task RoundNotFoundAsync()
{

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/SignTransactionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/SignTransactionTests.cs
+using NBitcoin;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -16,6 +17,30 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
{
public class SignTransactionTests
{
+ [Fact]
+ public async Task SuccessAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ using Key key = new();
+ Alice alice = WabiSabiFactory.CreateAlice(key: key);
+ round.Alices.Add(alice);
+ var coinjoin = Transaction.Create(Network.Main);
+ coinjoin.Inputs.Add(alice.Coins.First().Outpoint);
+ round.Coinjoin = coinjoin;
+ round.Phase = Phase.TransactionSigning;
+ arena.OnTryGetRound = _ => round;
+
+ var signedCoinJoin = coinjoin.Clone();
+ signedCoinJoin.Sign(key.GetBitcoinSecret(Network.Main), alice.Coins.First());
+
+ var req = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair(0, signedCoinJoin.Inputs[0].WitScript) });
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ handler.SignTransaction(req);
+ Assert.True(round.Coinjoin.Inputs.First().HasWitScript());
+ }
+
[Fact]
public async Task RoundNotFoundAsync()
{
@@ -23,7 +48,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
arena.OnTryGetRound = _ => null;
await using PostRequestHandler handler = new(new WabiSabiConfig(), new Prison(), arena, new MockRpcClient());
- var req = new TransactionSignaturesRequest(Guid.NewGuid(), Guid.NewGuid(), null!);
+ var req = new TransactionSignaturesRequest(Guid.NewGuid(), null!);
var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.SignTransaction(req));
Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);
}
@@ -36,7 +61,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
var round = WabiSabiFactory.CreateRound(cfg);
arena.OnTryGetRound = _ => round;
- var req = new TransactionSignaturesRequest(round.Id, Guid.NewGuid(), null!);
+ var req = new TransactionSignaturesRequest(round.Id, null!);
foreach (Phase phase in Enum.GetValues(typeof(Phase)))
{
if (phase != Phase.TransactionSigning)
@@ -50,18 +75,32 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
}
[Fact]
- public async Task AliceNotFoundAsync()
+ public async Task WrongCoinjoinSignatureAsync()
{
MockArena arena = new();
WabiSabiConfig cfg = new();
var round = WabiSabiFactory.CreateRound(cfg);
+ using Key key1 = new();
+ Alice alice1 = WabiSabiFactory.CreateAlice(key: key1);
+ using Key key2 = new();
+ Alice alice2 = WabiSabiFactory.CreateAlice(key: key2);
+ round.Alices.Add(alice1);
+ round.Alices.Add(alice2);
+ var coinjoin = Transaction.Create(Network.Main);
+ coinjoin.Inputs.Add(alice1.Coins.First().Outpoint);
+ coinjoin.Inputs.Add(alice2.Coins.First().Outpoint);
+ round.Coinjoin = coinjoin;
round.Phase = Phase.TransactionSigning;
arena.OnTryGetRound = _ => round;
- var req = new TransactionSignaturesRequest(round.Id, Guid.NewGuid(), null!);
+ // Submit the signature for the second alice to the first alice's input.
+ var signedCoinJoin = coinjoin.Clone();
+ signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), alice2.Coins.First());
+
+ var req = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair(0, signedCoinJoin.Inputs[0].WitScript) });
await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.SignTransaction(req));
- Assert.Equal(WabiSabiProtocolErrorCode.AliceNotFound, ex.ErrorCode);
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongCoinjoinSignature, ex.ErrorCode);
}
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
@@ -24,6 +24,7 @@ namespace WalletWasabi.WabiSabi.Backend.Models
IncorrectRequestedWeightCredentials,
TooMuchWeight,
ScriptNotAllowed,
- IncorrectRequestedAmountCredentials
+ IncorrectRequestedAmountCredentials,
+ WrongCoinjoinSignature
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/PostRequests/PostRequestHandler.cs
WalletWasabi/WabiSabi/Backend/PostRequests/PostRequestHandler.cs
@@ -153,16 +153,7 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
{
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
}
- if (round.Phase != Phase.TransactionSigning)
- {
- throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
- }
- if (!round.TryGetAlice(request.AliceId, out var alice))
- {
- throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceNotFound);
- }
-
- throw new NotImplementedException();
+ round.SubmitTransactionSignatures(request.InputWitnessPairs);
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
@@ -91,17 +91,13 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
public Guid Id { get; } = Guid.NewGuid();
public Phase Phase { get; set; } = Phase.InputRegistration;
public List<Alice> Alices { get; } = new();
+ public List<Bob> Bobs { get; } = new();
public Round? BlameOf { get; } = null;
public bool IsBlameRound => BlameOf is not null;
public ISet<OutPoint> BlameWhitelist { get; } = new HashSet<OutPoint>();
private object Lock { get; } = new();
public byte[] UnsignedTxSecret { get; }
-
- public bool TryGetAlice(Guid aliceId, [NotNullWhen(true)] out Alice? alice)
- {
- alice = Alices.FirstOrDefault(x => x.Id == aliceId);
- return alice is not null;
- }
+ public Transaction Coinjoin { get; set; }
public InputsRegistrationResponse RegisterAlice(
Alice alice,
@@ -251,11 +247,57 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
}
+ Bobs.Add(bob);
+
return new(
UnsignedTxSecret,
amountCredentialResponse,
weightCredentialResponse);
}
}
+
+ public void SubmitTransactionSignatures(IEnumerable<InputWitnessPair> inputWitnessPairs)
+ {
+ lock (Lock)
+ {
+ if (Phase != Phase.TransactionSigning)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+ foreach (var inputWitnessPair in inputWitnessPairs)
+ {
+ var index = (int)inputWitnessPair.InputIndex;
+ var witness = inputWitnessPair.Witness;
+
+ // If input is already signed, don't bother.
+ if (Coinjoin.Inputs[index].HasWitScript())
+ {
+ continue;
+ }
+
+ // Verify witness.
+ // 1. Copy UnsignedCoinJoin.
+ Transaction cjCopy = Transaction.Parse(Coinjoin.ToHex(), Network);
+
+ // 2. Sign the copy.
+ cjCopy.Inputs[index].WitScript = witness;
+
+ // 3. Convert the current input to IndexedTxIn.
+ IndexedTxIn currentIndexedInput = cjCopy.Inputs.AsIndexedInputs().Skip(index).First();
+
+ // 4. Find the corresponding registered input.
+ Coin registeredCoin = Alices.SelectMany(x => x.Coins).Single(x => x.Outpoint == cjCopy.Inputs[index].PrevOut);
+
+ // 5. Verify if currentIndexedInput is correctly signed, if not, return the specific error.
+ if (!currentIndexedInput.VerifyScript(registeredCoin, out ScriptError error))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongCoinjoinSignature);
+ }
+
+ // Finally add it to our CJ.
+ Coinjoin.Inputs[index].WitScript = witness;
+ }
+ }
+ }
}
}

MODIFY
WalletWasabi/WabiSabi/Models/InputWitnessPair.cs
WalletWasabi/WabiSabi/Models/InputWitnessPair.cs
@@ -9,13 +9,13 @@ namespace WalletWasabi.WabiSabi.Models
{
public class InputWitnessPair
{
- public InputWitnessPair(OutPoint input, WitScript witness)
+ public InputWitnessPair(uint inputIndex, WitScript witness)
{
- Input = input;
+ InputIndex = inputIndex;
Witness = witness;
}
- public OutPoint Input { get; }
+ public uint InputIndex { get; }
public WitScript Witness { get; }
}
}

MODIFY
WalletWasabi/WabiSabi/Models/TransactionSignaturesRequest.cs
WalletWasabi/WabiSabi/Models/TransactionSignaturesRequest.cs
@@ -8,16 +8,13 @@ namespace WalletWasabi.WabiSabi.Models
{
public class TransactionSignaturesRequest
{
- public TransactionSignaturesRequest(Guid roundId, Guid aliceId, IEnumerable<InputWitnessPair> inputWitnessPairs)
+ public TransactionSignaturesRequest(Guid roundId, IEnumerable<InputWitnessPair> inputWitnessPairs)
{
RoundId = roundId;
- AliceId = aliceId;
InputWitnessPairs = inputWitnessPairs;
}
public Guid RoundId { get; }
-
- public Guid AliceId { get; }
public IEnumerable<InputWitnessPair> InputWitnessPairs { get; }
}
}

Implement transaction signing