First WabiSabi API version
ADD
WalletWasabi.Tests/UnitTests/Crypto/Api/CredentialTests.cs
+using System.Linq;
+using NBitcoin;
+using NBitcoin.Secp256k1;
+using WalletWasabi.Crypto;
+using WalletWasabi.Crypto.Api;
+using WalletWasabi.Crypto.Randomness;
+using WalletWasabi.Crypto.ZeroKnowledge;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Crypto.Api
+{
+ public class CredentialTests
+ {
+ [Fact]
+ [Trait("UnitTest", "UnitTest")]
+ public void CredentialIssuance()
+ {
+ var numberOfCredentials = 3;
+ var rnd = new SecureRandom();
+ var sk = new CoordinatorSecretKey(rnd);
+
+ var client = new WabiSabiClient(sk.ComputeCoordinatorParameters(), numberOfCredentials, rnd);
+
+ {
+ var credentialRequest = client.AsCredentialRequestBuilder()
+ .RequestCredentialFor(Money.Zero)
+ .Build(new Transcript(new byte[0]));
+
+ Assert.True(credentialRequest.IsNullRequest);
+ Assert.Equal(numberOfCredentials, credentialRequest.Requested.Count());
+ var requested = credentialRequest.Requested.First();
+ Assert.Empty(requested.BitCommitments);
+ Assert.Equal(Money.Zero, credentialRequest.Balance);
+
+ // Issuer part
+ var issuer = new CredentialIssuer(sk, numberOfCredentials, rnd);
+
+ var credentialResponse = issuer.HandleRequest(credentialRequest);
+ client.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+ Assert.Equal(numberOfCredentials, client.Credentials.Count());
+ var issuedCredential = client.Credentials.First();
+ Assert.True(issuedCredential.Amount.IsZero);
+ }
+
+ {
+ var credentialRequest = client.AsCredentialRequestBuilder()
+ .RequestCredentialFor(Money.Zero)
+ .RequestCredentialFor(Money.Zero)
+ .Build(new Transcript(new byte[0]));
+
+ Assert.True(credentialRequest.IsNullRequest);
+ var requested = credentialRequest.Requested.ToArray();
+ Assert.Equal(numberOfCredentials, requested.Count());
+ Assert.Empty(requested[0].BitCommitments);
+ Assert.Empty(requested[1].BitCommitments);
+ Assert.Equal(Money.Zero, credentialRequest.Balance);
+
+ // Issuer part
+ var issuer = new CredentialIssuer(sk, numberOfCredentials, rnd);
+
+ var credentialResponse = issuer.HandleRequest(credentialRequest);
+ client.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+ var credentials = client.Credentials.ToArray();
+ Assert.NotEmpty(credentials);
+ Assert.Equal(6, credentials.Count());
+ Assert.All(credentials, x => Assert.True(x.Amount.IsZero));
+
+ credentialRequest = client.AsCredentialRequestBuilder()
+ .RequestCredentialFor(Money.Coins(1))
+ .RequestCredentialFor(Money.Zero)
+ .Build(new Transcript(new byte[0]));
+
+ Assert.False(credentialRequest.IsNullRequest);
+ var credentialRequested = credentialRequest.Requested.ToArray();
+ Assert.Equal(numberOfCredentials, credentialRequested.Count());
+ Assert.NotEmpty(credentialRequested[0].BitCommitments);
+ Assert.NotEmpty(credentialRequested[1].BitCommitments);
+
+ credentialResponse = issuer.HandleRequest(credentialRequest);
+ client.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+ credentials = client.Credentials.ToArray();
+ Assert.NotEmpty(credentials);
+ Assert.Equal(6, credentials.Count());
+ Assert.True(credentials[0].Amount.IsZero);
+ Assert.True(credentials[1].Amount.IsZero);
+ Assert.True(credentials[2].Amount.IsZero);
+ Assert.Equal(new Scalar(100_000_000), credentials[3].Amount);
+ Assert.True(credentials[4].Amount.IsZero);
+ }
+
+ {
+ var availableCredentials = client.Credentials.ToArray();
+
+ var credentialRequest = client.AsCredentialRequestBuilder()
+ .RequestCredentialFor(Money.Coins(0.5m))
+ .RequestCredentialFor(Money.Coins(0.5m))
+ .PresentCredentials(availableCredentials.Single(x => !x.Amount.IsZero))
+ .Build(new Transcript(new byte[0]));
+
+ Assert.False(credentialRequest.IsNullRequest);
+ var requested = credentialRequest.Requested.ToArray();
+ Assert.Equal(numberOfCredentials, requested.Count());
+ Assert.NotEmpty(requested[0].BitCommitments);
+ Assert.NotEmpty(requested[1].BitCommitments);
+ Assert.Equal(Money.Zero, credentialRequest.Balance);
+
+ // Issuer part
+ var issuer = new CredentialIssuer(sk, numberOfCredentials, rnd);
+
+ var credentialResponse = issuer.HandleRequest(credentialRequest);
+ client.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+ var credentials = client.Credentials.ToArray();
+ Assert.NotEmpty(credentials);
+ Assert.Equal(6, credentials.Count());
+
+ var nontNullCredentials = client.Credentials.Where(x => !x.Amount.IsZero).ToArray();
+ Assert.Equal(new Scalar(50_000_000), nontNullCredentials[0].Amount);
+ Assert.Equal(new Scalar(50_000_000), nontNullCredentials[1].Amount);
+ }
+
+ {
+ var client0 = new WabiSabiClient(sk.ComputeCoordinatorParameters(), numberOfCredentials, rnd);
+ var credentialRequest = client0.AsCredentialRequestBuilder()
+ .Build(new Transcript(new byte[0]));
+
+ var issuer = new CredentialIssuer(sk, numberOfCredentials, rnd);
+ var credentialResponse = issuer.HandleRequest(credentialRequest);
+ client0.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+
+ credentialRequest = client0.AsCredentialRequestBuilder()
+ .RequestCredentialFor(Money.Coins(1))
+ .Build(new Transcript(new byte[0]));
+
+ credentialResponse = issuer.HandleRequest(credentialRequest);
+ client0.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+
+ var nontNullCredentials = client0.Credentials.Where(x => !x.Amount.IsZero).ToArray();
+
+ credentialRequest = client0.AsCredentialRequestBuilder()
+ .PresentCredentials(nontNullCredentials)
+ .Build(new Transcript(new byte[0]));
+
+ // Issuer part
+ credentialResponse = issuer.HandleRequest(credentialRequest);
+ client0.HandleResponse(credentialResponse, new Transcript(new byte[0]));
+ var credentials = client0.Credentials.ToArray();
+ Assert.NotEmpty(credentials);
+ Assert.Equal(numberOfCredentials, credentials.Count());
+ }
+ }
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Tests/UnitTests/Crypto/ProofSystemTests.cs
WalletWasabi.Tests/UnitTests/Crypto/ProofSystemTests.cs
@@ -196,7 +196,6 @@ namespace WalletWasabi.Tests.UnitTests.Crypto
[InlineData((ulong)uint.MaxValue + 1, 32, false)]
[InlineData((ulong)uint.MaxValue + 1, 33, true)]
[InlineData(2099999997690000ul, 50, false)]
- [InlineData(2099999997690000ul, 51, true)]
public void CanProveAndVerifyCommitmentRange(ulong amount, int width, bool pass)
{
var rnd = new SecureRandom();
@@ -217,5 +216,36 @@ namespace WalletWasabi.Tests.UnitTests.Crypto
Assert.Throws<ArgumentOutOfRangeException>(() => ProofSystem.RangeProof(amountScalar, randomness, width, rnd));
}
}
+
+ [Fact]
+ [Trait("UnitTest", "UnitTest")]
+ public void CanProveAndVerifyZeroProofs()
+ {
+ var rnd = new SecureRandom();
+
+ var a0 = Scalar.Zero;
+ var r0 = rnd.GetScalar();
+ var Ma0 = a0 * Generators.Gg + r0 * Generators.Gh;
+
+ var a1 = Scalar.Zero;
+ var r1 = rnd.GetScalar();
+ var Ma1 = a1 * Generators.Gg + r1 * Generators.Gh;
+
+ var knowledge = new[]
+ {
+ ProofSystem.ZeroProof(Ma0, r0),
+ ProofSystem.ZeroProof(Ma1, r1)
+ };
+
+ var proofs = Prover.Prove(new Transcript(new byte[0]), knowledge, rnd);
+
+ var statements = new[]
+ {
+ ProofSystem.ZeroProof(Ma0),
+ ProofSystem.ZeroProof(Ma1)
+ };
+
+ Assert.True(Verifier.Verify(new Transcript(new byte[0]), statements, proofs));
+ }
}
}

ADD
WalletWasabi/Crypto/Api/CredentialIssuanceRequest.cs
+using System.Collections.Generic;
+using WalletWasabi.Crypto.Groups;
+
+namespace WalletWasabi.Crypto.Api
+{
+ public class CredentialIssuanceRequest
+ {
+ public CredentialIssuanceRequest(GroupElement ma, IEnumerable<GroupElement> bitCommitments)
+ {
+ Ma = ma;
+ BitCommitments = bitCommitments;
+ }
+
+ public GroupElement Ma { get; }
+ public IEnumerable<GroupElement> BitCommitments { get; }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Crypto/Api/ICredentialRequestBuilder.cs
+using NBitcoin;
+using WalletWasabi.Crypto.ZeroKnowledge;
+
+namespace WalletWasabi.Crypto.Api
+{
+ public interface ICredentialRequestBuilder
+ {
+ ICredentialRequestBuilder RequestCredentialFor(Money amount);
+
+ ICredentialRequestBuilder PresentCredentials(params Credential[] credential);
+
+ RegistrationRequest Build(Transcript transcript);
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Crypto/Api/RegistrationRequest.cs
+using System.Collections.Generic;
+using System.Linq;
+using NBitcoin;
+using WalletWasabi.Crypto.Groups;
+using WalletWasabi.Crypto.ZeroKnowledge;
+using WalletWasabi.Crypto.ZeroKnowledge.NonInteractive;
+
+namespace WalletWasabi.Crypto.Api
+{
+ public class RegistrationRequest
+ {
+ internal RegistrationRequest(
+ Money balance,
+ IEnumerable<CredentialPresentation> presented,
+ IEnumerable<CredentialIssuanceRequest> requested,
+ IEnumerable<Proof> proofs)
+ {
+ Balance = balance;
+ Presented = presented;
+ Requested = requested;
+ Proofs = proofs;
+ }
+
+ public Money Balance { get; }
+ public IEnumerable<CredentialPresentation> Presented { get; }
+ public IEnumerable<CredentialIssuanceRequest> Requested { get; }
+ public IEnumerable<Proof> Proofs { get; }
+
+ public bool IsNullRequest => Balance == Money.Zero && !Presented.Any();
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Crypto/Api/RegistrationResponse.cs
+using System;
+using System.Collections.Generic;
+using WalletWasabi.Crypto.ZeroKnowledge.NonInteractive;
+
+namespace WalletWasabi.Crypto.Api
+{
+ public class RegistrationResponse
+ {
+ public RegistrationResponse(IEnumerable<MAC> issuedCredentials, IEnumerable<Proof> proofs)
+ {
+ IssuedCredentials = issuedCredentials;
+ Proofs = proofs;
+ }
+
+ public IEnumerable<MAC> IssuedCredentials { get; }
+
+ public IEnumerable<Proof> Proofs { get; }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Crypto/Api/WabiSabiException.cs
+using System;
+using System.Runtime.Serialization;
+
+namespace WalletWasabi.Crypto.Api
+{
+ public enum WabiSabiErrorCode
+ {
+ Unspecified = 0,
+ SerialNumberAlreadyUsed = 1,
+ CoordinatorReceivedInvalidProofs = 2,
+ NegativeBalance = 3,
+ InvalidBitCommitment = 4,
+ ClientReceivedInvalidProofs = 5,
+ IssuedCredentialNumberMismatch = 6,
+ SerialNumberDuplicated = 7,
+ NotEnoughZeroCredentialToFillTheRequest = 8,
+ }
+
+ [Serializable]
+ public class WabiSabiException : Exception
+ {
+ public WabiSabiException()
+ {
+ }
+
+ public WabiSabiException(string? message) : base(message)
+ {
+ }
+
+ public WabiSabiException(WabiSabiErrorCode errorCode, string? message = null)
+ : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+
+ public WabiSabiException(string? message, Exception? innerException)
+ : base(message, innerException)
+ {
+ }
+
+ protected WabiSabiException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ public WabiSabiErrorCode ErrorCode { get; }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Crypto/Attribute.cs
+using NBitcoin;
+using NBitcoin.Secp256k1;
+using WalletWasabi.Crypto.Groups;
+using WalletWasabi.Crypto.Randomness;
+
+namespace WalletWasabi.Crypto
+{
+ public class Attribute
+ {
+ private Attribute(Scalar amount, Scalar randomness)
+ {
+ Randomness = randomness;
+ Ma = amount * Generators.Gg + randomness * Generators.Gh;
+ }
+
+ public static Attribute FromMoney(Money money, WasabiRandom rng)
+ => new Attribute(new Scalar((ulong)money.Satoshi), rng.GetScalar(allowZero: false));
+
+ public Scalar Randomness { get; }
+ public GroupElement Ma { get; }
+ }
+}

MODIFY
WalletWasabi/Crypto/Extensions.cs
WalletWasabi/Crypto/Extensions.cs
@@ -3,14 +3,22 @@ using WalletWasabi.Helpers;
using WalletWasabi.Crypto.Groups;
using WalletWasabi.Crypto.ZeroKnowledge.LinearRelation;
using WalletWasabi.Crypto;
+using NBitcoin.Secp256k1;
+using NBitcoin;
namespace System.Linq
{
public static class Extensions
{
+ public static Scalar Sum(this IEnumerable<Scalar> scalars) =>
+ scalars.Aggregate(Scalar.Zero, (s, acc) => s + acc);
+
public static GroupElement Sum(this IEnumerable<GroupElement> groupElements) =>
groupElements.Aggregate(GroupElement.Infinity, (ge, acc) => ge + acc);
+ public static Money ToMoney(this Scalar scalar) =>
+ Money.Satoshis(((ulong)scalar.d1 << 32) | scalar.d0);
+
public static IEnumerable<TResult> Zip<TFirst, TSecond, TThird, TResult>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, IEnumerable<TThird> third, Func<TFirst, TSecond, TThird, TResult> resultSelector)
{
Guard.NotNull(nameof(first), first);

MODIFY
WalletWasabi/Crypto/ZeroKnowledge/ProofSystem.cs
WalletWasabi/Crypto/ZeroKnowledge/ProofSystem.cs
@@ -95,9 +95,9 @@ namespace WalletWasabi.Crypto.ZeroKnowledge
var bitCommitments = bits.Zip(randomness, (b, r) => b * Generators.Gg + r * Generators.Gh);
var columns = width * 3 + 1; // three witness terms per bit and one for the commitment
- int bitColumn(int i) => 3 * i + 1;
- int rndColumn(int i) => bitColumn(i) + 1;
- int productColumn(int i) => bitColumn(i) + 2;
+ static int BitColumn(int i) => 3 * i + 1;
+ static int RndColumn(int i) => BitColumn(i) + 1;
+ static int ProductColumn(int i) => BitColumn(i) + 2;
// Construct witness vector. First term is r from Ma = a*Gg + r*Gh. This
// is followed by 3 witness terms per bit commitment, the bit b_i, the
@@ -106,9 +106,9 @@ namespace WalletWasabi.Crypto.ZeroKnowledge
witness[0] = r;
foreach ((Scalar b_i, Scalar r_i, int i) in bits.Zip(randomness, Enumerable.Range(0, width), (x, y, z) => (x, y, z)))
{
- witness[bitColumn(i)] = b_i;
- witness[rndColumn(i)] = r_i;
- witness[productColumn(i)] = r_i * b_i;
+ witness[BitColumn(i)] = b_i;
+ witness[RndColumn(i)] = r_i;
+ witness[ProductColumn(i)] = r_i * b_i;
}
return (new Knowledge(RangeProof(ma, bitCommitments), new ScalarVector(witness)), bitCommitments);
@@ -122,9 +122,8 @@ namespace WalletWasabi.Crypto.ZeroKnowledge
public static Statement RangeProof(GroupElement ma, IEnumerable<GroupElement> bitCommitments)
{
- var width = bitCommitments.Count();
- Guard.True(nameof(width), width >= 0); // a width of 0 means no bits are set (commitment to 0)
- Guard.True(nameof(width), width <= 255); // 256 is tautological for the order of secp256k1
+ var width = bitCommitments.Count(); // can be 0
+ Guard.InRangeAndNotNull(nameof(width), width, 0, Constants.RangeProofWidth); // TODO ulong support
var rows = width * 2 + 1; // two equations per bit, and one for the sum
var columns = width * 3 + 1 + 1; // three witness components per bit and one for the Ma randomness, plus one for the public inputs
@@ -148,24 +147,24 @@ namespace WalletWasabi.Crypto.ZeroKnowledge
// as a generator for a -1 term ) and the remaining are generators to
// be used with 1+3n terms:
// ( r, b_0, r_0, rb_0, b_1, r_1, rb_1, ..., b_n, r_n, rb_n)
- int bitColumn(int i) => 3 * i + 2; // column for b_i witness term
- int rndColumn(int i) => bitColumn(i) + 1; // column for r_i witness term
- int productColumn(int i) => bitColumn(i) + 2; // column for rb_i witness term
- int bitRepresentationRow(int i) => 2 * i + 1; // row for B_i representation proof
- int bitSquaredRow(int i) => bitRepresentationRow(i) + 1; // row for [ b*(B_i-Gg) - rb*Gh <=> b = b*b ] proof
+ static int BitColumn(int i) => 3 * i + 2; // column for b_i witness term
+ static int RndColumn(int i) => BitColumn(i) + 1; // column for r_i witness term
+ static int ProductColumn(int i) => BitColumn(i) + 2; // column for rb_i witness term
+ static int BitRepresentationRow(int i) => 2 * i + 1; // row for B_i representation proof
+ static int BitSquaredRow(int i) => BitRepresentationRow(i) + 1; // row for [ b*(B_i-Gg) - rb*Gh <=> b = b*b ] proof
// For each bit, add two equations and one term to the first equation.
var B = bitCommitments.ToArray();
for (int i = 0; i < bitCommitments.Count(); i++)
{
// Add [ -r_i * 2^i * Gh ] term to first equation.
- equations[0, rndColumn(i)] = Scalar.Zero.CAddBit((uint)i, 1) * Generators.Gh.Negate();
+ equations[0, RndColumn(i)] = Scalar.Zero.CAddBit((uint)i, 1) * Generators.Gh.Negate();
// Add equation proving B is a Pedersen commitment to b:
// [ B = b*Gg + r*Gh ]
- equations[bitRepresentationRow(i), 0] = B[i];
- equations[bitRepresentationRow(i), bitColumn(i)] = Generators.Gg;
- equations[bitRepresentationRow(i), rndColumn(i)] = Generators.Gh;
+ equations[BitRepresentationRow(i), 0] = B[i];
+ equations[BitRepresentationRow(i), BitColumn(i)] = Generators.Gg;
+ equations[BitRepresentationRow(i), RndColumn(i)] = Generators.Gh;
// Add an equation:
// [ O = b*(B - Gg) - rb*Gh ]
@@ -179,8 +178,8 @@ namespace WalletWasabi.Crypto.ZeroKnowledge
//
// in the verification equation we require that the following terms
// cancel out (public input point is O):
- equations[bitSquaredRow(i), bitColumn(i)] = B[i] - Generators.Gg;
- equations[bitSquaredRow(i), productColumn(i)] = Generators.Gh.Negate();
+ equations[BitSquaredRow(i), BitColumn(i)] = B[i] - Generators.Gg;
+ equations[BitSquaredRow(i), ProductColumn(i)] = Generators.Gh.Negate();
}
return new Statement(equations);

MODIFY
WalletWasabi/Helpers/Constants.cs
WalletWasabi/Helpers/Constants.cs
@@ -92,6 +92,12 @@ namespace WalletWasabi.Helpers
"/Satoshi:0.16.0/",
};
+ public static readonly int RangeProofWidth = (int)Math.Log2(MaximumNumberOfSatoshis);
+
+ public static Money MinimumCredentailAmount = Money.Satoshis(10_000);
+
+ public static Money MaximumCredentailAmount = Money.Satoshis(Constants.MaximumNumberOfSatoshis);
+
public static string ClientSupportBackendVersionText => ClientSupportBackendVersionMin == ClientSupportBackendVersionMax
? ClientSupportBackendVersionMin
: $"{ClientSupportBackendVersionMin} - {ClientSupportBackendVersionMax}";

First WabiSabi API version