BTC#: SEC Serialisation

Series: BTC# – Learning to Program Bitcoin in C#

« Previous: Endianness

Next: DER Serialisation »

So far, we’ve just created objects like public keys and signatures and manipulated them in memory. Soon, we’ll want to serialise them to the network or to disk as we construct scripts and transactions.

BitcoinMechanics.png

SEC for Public Keys

The first type of serialisation Programming Bitcoin introduces is the SEC format used for public keys. The uncompressed format is very straightforward: a marker byte to indicate that it’s uncompressed followed by the x- and y-values of the public key in big-endian hexadecimal.

SecSerialisation.png

The compressed format takes advantage of the fact that the elliptic curve is symmetrical. If you know the x-value, you can narrow the y-value down to two possibilities, one even and one odd. The compressed serialisation consists of a marker byte indicating the parity (the even- or odd-ness) of the y-value followed by the x-value serialised as above.

I’ve added a SerialisationFormat enum to hold the different serialisation rules we’ll come across and a static Serialisation class to hold constants for things like the values of the marker bytes.

public enum SerialisationFormat
{
    Uncompressed = 0,
    Compressed = 1,
}
public static class Serialisation
{
    public const byte SEC_MARKER_COMPRESSED_EVEN = 2;
    public const byte SEC_MARKER_COMPRESSED_ODD = 3;
    public const byte SEC_MARKER_UNCOMPRESSED = 4;
}

The serialisation code is on the PublicKey class and makes use of the Endianness helpers from the previous post.

public byte[] ToSecFormat(SerialisationFormat format)
{
    return (format & SerialisationFormat.Compressed) == SerialisationFormat.Compressed ?
        ToCompressedSecFormat() :
        ToUncompressedSecFormat();
}

private byte[] ToUncompressedSecFormat()
{
    return (new byte[] { Serialisation.SEC_MARKER_UNCOMPRESSED })
        .Concat(Point.X.Value.ToByteArray(ByteArrayFormat.BigEndianUnsigned, 32))
        .Concat(Point.Y.Value.ToByteArray(ByteArrayFormat.BigEndianUnsigned, 32))
        .ToArray();
}

private byte[] ToCompressedSecFormat()
{
    var markerByte = Point.Y.Value % 2 == 0 ?
        Serialisation.SEC_MARKER_COMPRESSED_EVEN :
        Serialisation.SEC_MARKER_COMPRESSED_ODD;

    return (new byte[] { markerByte })
        .Concat(Point.X.Value.ToByteArray(ByteArrayFormat.BigEndianUnsigned, 32))
        .ToArray();
}

SEC serialisation is very simple. Deserialising the uncompressed form is also very simple because all the necessary data is already present. Deserialising the compressed form is a little bit harder because the y-value needs to be derived. One necessary extra is the ability to find the square root of a finite field element. The book shows the derivation but the resulting code is quite simple.

public FiniteFieldElement Sqrt()
{
    return this.Pow((Order + 1) / 4);
}

The Parse method is a static method that returns a PublicKey.

public static PublicKey Parse(byte[] secBuffer)
{
    var marker = secBuffer[0];

    if(marker < Serialisation.SEC_MARKER_MIN || marker > Serialisation.SEC_MARKER_MAX)
    {
        throw new ArgumentException($"Invalid marker byte. {marker:x2} supplied.");
    }

    var curve = Secp256k1.Curve;
    FiniteFieldElement x, y;

    if (marker == Serialisation.SEC_MARKER_UNCOMPRESSED)
    {
        if (secBuffer.Length != Serialisation.SEC_LENGTH_UNCOMPRESSED)
        {
            throw new ArgumentException($"Uncompressed SEC buffer (prefix {marker:x2}) must be {Serialisation.SEC_LENGTH_UNCOMPRESSED} bytes long. {secBuffer.Length} bytes supplied.");
        }
        x = new FiniteFieldElement(
            secBuffer.Segment(1, 32).ToBigInteger(ByteArrayFormat.BigEndianUnsigned),
            Secp256k1.P
        );
        y = new FiniteFieldElement(
            secBuffer.Segment(33, 32).ToBigInteger(ByteArrayFormat.BigEndianUnsigned),
            Secp256k1.P
        );
        return new PublicKey(new EllipticCurveFiniteFieldPoint(x, y, curve));
    }

    if (secBuffer.Length != Serialisation.SEC_LENGTH_COMPRESSED)
    {
        throw new ArgumentException($"Compressed SEC buffer (prefix {marker:x2}) must be {Serialisation.SEC_LENGTH_COMPRESSED} bytes long. {secBuffer.Length} bytes supplied.");
    }
    x = new FiniteFieldElement(
        secBuffer.Segment(1, 32).ToBigInteger(ByteArrayFormat.BigEndianUnsigned),
        Secp256k1.P
    );
    var alpha = x.Pow(3) + curve.B;
    var beta = alpha.Sqrt();
    y = (marker == 2 ^ beta.Value % 2 == 0) ?
        new FiniteFieldElement(Secp256k1.P - beta.Value, Secp256k1.P) :
        beta;
    return new PublicKey(new EllipticCurveFiniteFieldPoint(x, y, curve));
}

The code checks the marker byte and the length of the buffer supplied – 65 bytes when uncompressed, 33 when compressed. If that’s OK, it segments the byte array into the appropriate pieces and deserialises them into BigInteger objects. For compressed data it then works out what the y-value is and returns the new public key.

« Previous: Endianness

Next: DER Serialisation »

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s