Serialize using compressed format
MODIFY
WalletWasabi.Tests/UnitTests/Crypto/GroupElementTests.cs
WalletWasabi.Tests/UnitTests/Crypto/GroupElementTests.cs
@@ -293,7 +293,7 @@ namespace WalletWasabi.Tests.UnitTests.Crypto
Assert.ThrowsAny<ArgumentException>(() => GroupElement.FromBytes(FillByteArray(length: 32, character: 0)));
Assert.ThrowsAny<ArgumentException>(() => GroupElement.FromBytes(FillByteArray(length: 63, character: 0)));
- var infinity = GroupElement.FromBytes(FillByteArray(length: 64, character: 0));
+ var infinity = GroupElement.FromBytes(FillByteArray(length: 33, character: 0));
Assert.True(infinity.IsInfinity);
Assert.ThrowsAny<ArgumentException>(() => GroupElement.FromBytes(FillByteArray(length: 64, character: 1)));
Assert.ThrowsAny<ArgumentException>(() => GroupElement.FromBytes(FillByteArray(length: 64, character: 2)));

MODIFY
WalletWasabi/Crypto/GroupElement.cs
WalletWasabi/Crypto/GroupElement.cs
@@ -121,34 +121,45 @@ namespace WalletWasabi.Crypto
public GroupElement Negate() => new GroupElement(Ge.Negate());
- public byte[] ToBytes() => ByteHelpers.Combine(Ge.x.ToBytes(), Ge.y.ToBytes());
-
- public static GroupElement FromBytes(byte[] bytes)
+ public byte[] ToBytes()
{
- Guard.Same($"{nameof(bytes)}.{nameof(bytes.Length)}", 64, bytes.Length);
-
- // Only infinity can have zeros.
- // If one defines infinity in the constructor, but with non-zero coordinates it'll zero them out.
- // If one defines zero coordinates but not infinity, the constructor will throw invalid variable error.
- if (bytes.All(b => b == 0))
- {
- return Infinity;
- }
+ // Buffer to store the serialized Group Element in its **compressed** format.
+ // It requires 32 bytes for the 256bits `x` coordinate and an extra byte for
+ // the EVEN/ODD flag.
+ const int CompressedLength = 32 + 1;
+ Span<byte> buffer = new byte[CompressedLength];
- var xBytes = bytes.Take(32).ToArray();
- var yBytes = bytes.Skip(32).ToArray();
+ // We cannot assume the Group Element was already normalized somewhere else.
+ var x = Ge.x.NormalizeVariable();
+ var y = Ge.y.NormalizeVariable();
- if (!FE.TryCreate(xBytes, out FE x))
+ buffer[0] = (Ge.IsInfinity, y.IsOdd) switch
{
- throw new ArgumentException("Couldn't create x field element.", nameof(bytes));
+ (true, _ ) => (byte)0, // see http://www.secg.org/sec1-v2.pdf sections 2.3.3-4:
+ (false, true ) => GE.SECP256K1_TAG_PUBKEY_ODD,
+ (false, false) => GE.SECP256K1_TAG_PUBKEY_EVEN,
+ };
+ x.WriteToSpan(buffer[1..]);
+ return buffer.ToArray();
}
- if (!FE.TryCreate(yBytes, out FE y))
+ public static GroupElement FromBytes(byte[] bytes)
{
- throw new ArgumentException("Couldn't create y field element.", nameof(bytes));
- }
+ const int CompressedLength = 32 + 1;
+ Guard.Same($"{nameof(bytes)}.{nameof(bytes.Length)}", CompressedLength, bytes.Length);
- return new GroupElement(new GE(x, y));
+ GroupElement Parse(Span<byte> buffer, bool isOdd) =>
+ FE.TryCreate(buffer, out var x) && GE.TryCreateXOVariable(x, isOdd, out var ge)
+ ? new GroupElement(ge)
+ : throw new InvalidOperationException("Group element could not be deserialized");
+
+ return bytes[0] switch
+ {
+ 0 => GroupElement.Infinity,
+ GE.SECP256K1_TAG_PUBKEY_ODD => Parse(bytes[1..], isOdd: true),
+ GE.SECP256K1_TAG_PUBKEY_EVEN => Parse(bytes[1..], isOdd: false),
+ _ => throw new ArgumentException("Argument is not a valid group element")
+ };
}
}
}

Serialize using compressed format