BTC#: Transaction Objects

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

« Previous: Base 58 Encoded Addresses

Next: Script Parsing »

Now for the real meat. The work we’ve done so far, the maths and the serialisation, has provided the scaffolding for what we really want to do, which is transfer value.

BankerUtxos.png

We talked earlier about how there are no “coins” in Bitcoin. What we’re spending are Unspent Transaction Outputs (UTXOs). A transaction is a collection of inputs, which point back to previous UTXOs and a collection of outputs, which become the new UTXOs.

Each output is created with a locking script, often called the public key script, which is a bit like a puzzle to be solved. Anyone who solves the puzzle can spend that output. Each input contains an unlocking script, often called the signature script, which contains the solution to the puzzle.

The classes created in the Transactions chapter of Programming Bitcoin are Transaction, TxInput, TxOutput, and Script. This is how they fit together.

TransactionClasses.png

To start with I created very simple classes that contained the relevant fields.

public class Transaction
{
    public uint Version { get; }
    public TxInput[] Inputs { get; }
    public TxOutput[] Outputs { get; }
    public uint Locktime { get; }
}
public class TxInput
{
    public BigInteger PreviousTxId { get; }
    public uint PreviousTxIndex { get; }
    public Script ScriptSig { get; }
    public uint Sequence { get; }
}
public class TxOutput
{
    public ulong Amount { get; }
    public Script ScriptPubKey { get; }
}
public class Script
{
    private byte[] RawScript { get; }
}

At this stage I’m just storing the script as a byte array. This is fine for parsing and serialising the basic objects. We’ll flesh this out a bit when we talk specifically about scripts.

All of the integer fields have been stored as unsigned integers because that maps more closely to the serialised form and there’s no benefit to using signed values. What each field means is discussed in the book.

Binary Streams

To read and write transaction data, each of these classes will have a static Parse method that creates a new instance and a Serialise method that write out the data. The Parse method will use a BinaryReader and the Serialise method will use a BinaryWriter.

These classes wrap streams so they can be used to read and write binary data from the network, from a file, or from a byte array held in memory, which we’ll use in testing. They come with a number of convenience methods, such as BinaryReader.ReadUInt64, that read the correct number of bytes and deserialise them into the appropriate type.

Variable Integers

Bitcoin uses one data type that is not catered for with these convenience methods – the variable integer, or VarInt. For example, the input and output counts on the transaction are VarInts.

VarInts can hold numbers up to 64-bits wide, the equivalent of a long in C#, but without using the full width when a smaller number is encoded. If the number is below 253, a single byte is used. For higher numbers, the first byte indicates the width and remaining bytes hold the value.

I’ve used extension methods on BinaryReader and Binary Writer to handle these fields.

public static class BinaryReaderExtension
{
    public static ulong ReadVarInt(this BinaryReader reader)
    {
        var leadingByte = reader.ReadByte();
        if (leadingByte == 253) //fd
        {
            return reader.ReadUInt16();
        }
        if (leadingByte == 254) //fe
        {
            return reader.ReadUInt32();
        }
        if (leadingByte == 255) //ff
        {
            return reader.ReadUInt64();
        }
        return leadingByte;
    }
}

Parsing

Each class has a static Parse method that takes a BinaryReader and returns an instance. Here’s the Script.Parse method.

public static Script Parse(BinaryReader reader)
{
    var scriptLength = (int)reader.ReadVarInt();

    return new Script(reader.ReadBytes(scriptLength));
}

The Script structure has a variable-length integer that contains the length of the script. We read in that number of bytes. At this stage, I’m just storing the byte array. We’ll do more with it later.

There’s some serialised transaction data in the book and I’ve used that to create unit tests.

[TestClass]
public class ScriptTests
{
    [TestMethod]
    public void Parse_Length()
    {
        var scriptHex = "6a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937";
        var reader = new BinaryReader(new MemoryStream(scriptHex.GetBytesFromHex()));

        var script = Script.Parse(reader);
        var expectedLength = 106;
        var actualLength = script.Length;

        Assert.AreEqual(expectedLength, actualLength);
    }
}

Working outwards, I’ve created Parse methods on TxInput, TxOutput, and Transaction.

public static TxOutput Parse(BinaryReader reader)
{
    var amount = reader.ReadUInt64();
    var scriptPubKey = Script.Parse(reader);

    return new TxOutput(amount, scriptPubKey);
}

At each layer, the method takes the reader, consumes the data it needs and passes the reader down to the child method.

public static Transaction Parse(BinaryReader reader)
{
    var version = reader.ReadUInt32();
    var inputCount = reader.ReadVarInt();
    var inputs = new TxInput[inputCount];
    for (ulong i = 0; i < inputCount; i++)
    {
        inputs[i] = TxInput.Parse(reader);
    }
    var outputCount = reader.ReadVarInt();
    var outputs = new TxOutput[outputCount];
    for (ulong j = 0; j < outputCount; j++)
    {
        outputs[j] = TxOutput.Parse(reader);
    }
    var locktime = reader.ReadUInt32();

    return new Transaction(version, inputs, outputs, locktime);
}

Serialisation

The serialisation code is very similar, writing out the fields to a BinaryWriter in the same way.

public void Serialise(BinaryWriter writer)
{
    writer.Write(Version);
    writer.WriteVarInt((ulong)Inputs.Length);
    for (int i = 0; i < Inputs.Length; i++)
    {
        Inputs[i].Serialise(writer);
    }
    writer.WriteVarInt((ulong)Outputs.Length);
    for (int j = 0; j < Outputs.Length; j++)
    {
        Outputs[j].Serialise(writer);
    }
    writer.Write(Locktime);
}

I haven’t shown all the code here. Get the full project from my LearningBitcoin repo on GitHub.

« Previous: Base 58 Encoded Addresses

Next: Script Parsing »

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