Diabelski młyn w Helsinkach

Protokół Bitcoina od środka: Ogłaszamy transakcję

W poprzednich wpisach z cyklu o protokole Bitcoina udało nam się rozłożyć na czynniki pierwsze komunikację z siecią, algorytm tworzenia adresów, czy koncepcję autoryzacji transakcji. Przyszedł czas na podsumowanie zdobytej do tej pory wiedzy i zaimplementowanie prostej aplikacji, której celem będzie skonstruowanie transakcji i ogłoszenie jej w sieci. Zatem do dzieła!

Przewodnik po wpisie

Jeżeli znajdujesz się w tym miejscu po lekturze poprzednich części z cyklu, to doskonale! Przeczytasz tutaj małe przypomnienie opisywanych wcześniej koncepcji idących za protokołem Bitcoina, a ponadto zapoznasz się z zastosowaniem ich w praktyce. Przedstawię tutaj bowiem nieskomplikowaną aplikację, która ma jeden prosty cel: wysyłać w świat Bitcoiny. Aplikacja ta to zebrany w jednym miejscu kod źródłowy, który pojawiał się w poprzednich wpisach, rozszerzony o implementację omawianej ostatnio wiadomości reprezentującej transakcję oraz koncepcji jej podpisu.

Jeżeli znajdujesz się w tym miejscu po raz pierwszy to równie perfekcyjnie – śmiało możesz kontynuować czytanie wpisu. Tam, gdzie zrozumienie pewnych koncepcji będzie wymagało przyswojenia wiedzy omawianej wcześniej – umieszczę link do odpowiedniego wpisu.

Cały omawiany tutaj kod umieściłem na GitHubie. Śmiało możesz go pobrać, testować i analizować równolegle z czytaniem wpisu. Będę uradowany, jeśli masz do niego jakieś uwagi lub komentarze!

Przydałoby się konto

Głównym celem aplikacji jest wykonanie przelewu w sieci Bitcoin. Jak wiadomo, z pustego to i Salomon nie naleje – przydałoby się konto, z którego wykonamy przelew. Korzystając z kodu opisywanego w jednym z poprzednich wpisów, utworzyłem konto o takich parametrach (klucz prywatny to nic innego jak wynik SHA256(“mikoleusz.pl”):

Klucz prywatny: 0x0E C6 4A 44 D9 D9 E0 CB 05 62 B7 BE F2 61 58 D7 21 5F 23 5B F0 8B F1 8B 67 BA 7B 5B BE 73 02 8C
Adres konta: 1Khro6tJBWbH1fPTLcnbmvuKiXgXhjVxUM

No i kilka Satoshi

Przydałoby się jeszcze zapewnić środki na nowo utworzonym koncie. Sposobów na to jest kilka – możesz przeczytać o nich w tym miejscu. Na potrzeby niniejszego wpisu wpłaciłem na to konto równowartość ok. 4 zł.

Jaki jest plan?

Plan wysłania wyżej wspomnianych 4-ech złotych w świat składa się z kilku etapów:

  1. Połączenie z dowolnym nodem sieci Bitcoin
  2. Utworzenie wiadomości “tx” reprezentującej transakcję
  3. Wysłanie wiadomości do node’a, z którym się połączyliśmy
  4. Obserwowanie statusu transakcji na stronie Blockchain

Pierwszy punkt zawiera się w jednym z poprzednich wpisów. Tam także znajduje się implementacja połączenia z siecią. Co do reszty, wiemy już jak zbudowana jest wiadomość “tx” oraz w jaki sposób możemy udowodnić, że jesteśmy posiadaczami środków na koncie, z którego wykonujemy transakcję.

Czas doimplementować brakujące elementy 🙂

Implementacja wiadomości “tx”

Z budowy wiadomości transakcyjnej łatwo wydzielić kilka podstawowych komponentów. Nagłówek, który znajduje się w każdej wiadomości obecnej w sieci Bitcoin, wersja wiadomości, lista transakcji wchodzących, wychodzących i moment wykonania. Przykładowa implementacja klasy reprezentującej tę wiadomość mogłaby wyglądać następująco:

public class TxMessage
{
    public Header Header { get; }
    public uint Version { get; }
    public VarInt TxInsCount { get; set; }
    public List<TxIn> TxIns { get; set; }
    public VarInt TxOutsCount { get; set; }
    public List<TxOut> TxOuts { get; set; }
    public uint LockTime { get; }

    //...
}

Przydałaby się jeszcze funkcjonalność tworzenia pustej wiadomości i – skoro zamierzamy ją wysyłać dalej – jakiejś zgrabnej serializacji:

public class TxMessage
{
    //...

    public TxMessage()
    {
        Header = new Header("tx");
        Version = 1;
        TxInsCount = new VarInt(0);
        TxIns = new List<TxIn>();
        TxOutsCount = new VarInt(0);
        TxOuts = new List<TxOut>();
        LockTime = 0x00;
    }

    public void ComputeHeader()
    {
        var payload = GetPayload();
        
        Header.PayloadLength = Convert.ToUInt32(payload.Length);
        Header.CalculateChecksum(payload);
    }

    private byte[] GetPayload()
    {
        var payload = BitConverter.GetBytes(Version)
            .Concat(TxInsCount.ToBytes()).ToArray();

        foreach (var txIn in TxIns)
        {
            payload = payload.Concat(txIn.ToBytes()).ToArray();
        }

        payload = payload.Concat(TxOutsCount.ToBytes()).ToArray();

        foreach (var txOut in TxOuts)
        {
            payload = payload.Concat(txOut.ToBytes()).ToArray();
        }

        payload = payload
            .Concat(BitConverter.GetBytes(LockTime)).ToArray();

        return payload;
    }

    public byte[] ToBytes()
    {
        ComputeHeader();

        var header = Header.ToBytes();
        var payload = GetPayload();

        return header.Concat(payload).ToArray();
    }    

    //...
}

Mamy już pewien zarys transakcji. Pozostało nam jeszcze zaimplementować klasy reprezentujące środki wchodzące i z niej wychodzące. No i najbardziej zawiłą rzecz w całym zamieszaniu – jak odcisnąć pieczęć na tym magicznym ciągu bajtów tak, aby udowodnić, że to my mamy prawo dysponować środkami, które do naszej transakcji wchodzą.

Środki wchodzące

Środki, które wchodzą do transakcji reprezentowane są przez strukturę o nazwie “TxIn”. Składa się ona tak na prawdę w dwóch podstawowych i jednocześnie kluczowych elementów, które pozwalają stwierdzić, czy mamy prawo wykorzystywać zgromadzone środki. Pierwszy taki element, to identyfikator transakcji, dzięki której konto, z którego chcemy wypłacić środki zostało zasilone. W naszym przypadku, będzie to transakcja, którą zasiliłem konto wspomnianymi 4 zł. Transakcja ta otrzymała skrót c9bc2a9fc27b4c188891b94a27878257c51c1d8629ccb6ae5ebc2f0a51ce2b83. Drugim ważnym elementem jest “ScriptSig”. Jest to fragment skryptu, który w procesie weryfikacji doklejany jest do skryptu pochodzącego z poprzedniej transakcji i uruchamiany. Jeżeli wykonanie tego skryptu zakończy się sukcesem, mamy prawo dysponować środkami. Większość skryptów skonstruowanych jest w ten sposób, że wymagane jest, aby “ScriptSig” składał się z następujących po sobie podpisanego kluczem prywatnym skrótu z transakcji oraz klucza publicznego. O tym jak dokładnie przebiega proces autoryzowania – odsyłam do poprzedniego wpisu, a o tym jak się ma klucz prywatny do publicznego i finalnie adresu Bitcoin dokładnie w to miejsce.

Implementacja opisywanej struktury mogłaby prezentować się następująco:

public class TxIn
{
    public OutPoint PreviousOutput { get; }
    public VarInt ScriptSigLength { get; set; }
    public byte[] ScriptSig { get; set; }
    public uint Sequence { get; }

    public TxIn(OutPoint previousOutput, byte[] scriptSig)
    {
        PreviousOutput = previousOutput;
        ScriptSigLength = new VarInt(Convert.ToUInt64(scriptSig.Length));
        ScriptSig = scriptSig;

        Sequence = 0xFFFFFFFF;
    }

    public void CreateSignatureScript(byte[] signature, byte[] publicKey)
    {
        const byte SIGHASH_ALL = 0x01;

        var extendedSignature = 
            signature.Concat(new[] { SIGHASH_ALL }).ToArray();

        var extendedSignatureLength = 
            new[] { Convert.ToByte(extendedSignature.Length) };

        var publicKeyLength = new[] { Convert.ToByte(publicKey.Length) };

        ScriptSig =
            extendedSignatureLength.Concat(extendedSignature)
                .Concat(publicKeyLength)
                .Concat(publicKey)
                .ToArray();

        ScriptSigLength = new VarInt(Convert.ToUInt64(ScriptSig.Length));
    }

    public byte[] ToBytes()
    {
        return PreviousOutput.ToBytes()
                .Concat(ScriptSigLength.ToBytes())
                .Concat(ScriptSig)
                .Concat(BitConverter.GetBytes(Sequence))
                .ToArray();
    }
}

Występująca tutaj struktura “OutPoint” reprezentuje wspomniany identyfikator poprzedniej transakcji:

public class OutPoint
{
    public byte[] Hash { get; }
    private uint Index { get; }

    public OutPoint(byte[] hash, uint index)
    {
        Hash = new byte[hash.Length];

        hash.CopyTo(Hash, 0);
        Hash = Hash.Reverse().ToArray();

        Index = index;
    }

    public byte[] ToBytes()
    {
        return Hash
                .Concat(BitConverter.GetBytes(Index))
                .ToArray();
    }
}

Sam proces podpisywania transakcji, czyli w tym przypadku generowania ciągu bajtów signature jako argument metody CreateSignatureScript omówię w dalszej części wpisu, bowiem odbywa się on w momencie, w którym wszystkie pola transakcji są już uzupełnione.

Środki wychodzące

Kolejnym krokiem w budowie transakcji jest zdefiniowanie, gdzie nasze środki mają się udać. Służy do tego struktura o nazwie “TxOut” i także składa się z dwóch kluczowych elementów. Pierwszy to kwota, którą chcemy przelać, podawana w Satoshi, zaś drugi to skrypt określający warunki wydania środków w przyszłości. Standardowo jest to skrypt, w uproszczeniu mówiący “te środki może wykorzystać tylko ta osoba, która udowodni, że jest w posiadaniu prywatnego klucza pasującego do publicznego, który tutaj podaję”. Nic nie stoi na przeszkodzie, aby wygenerować skrypt mówiący “kto pierwszy ten lepszy, nie wymagam żadnych kluczy”.

Zobaczmy zatem jak można zaimplementować “TxOut”:

public class TxOut
{
    public ulong Value { get; }
    public VarInt ScriptPubKeyLength { get; }
    public ScriptPubKey ScriptPubKey { get; }

    public TxOut(ulong value, ScriptPubKey scriptPubKey)
    {
        Value = value;
        ScriptPubKeyLength = 
            new VarInt(Convert.ToUInt64(scriptPubKey.Length));
        ScriptPubKey = scriptPubKey;
    }

    public byte[] ToBytes()
    {
        var result = BitConverter.GetBytes(Value)
            .Concat(ScriptPubKeyLength.ToBytes())
            .Concat(ScriptPubKey.ToBytes())
            .ToArray();

        return result;
    }
}

“ScriptPubKey” to nic innego jak wygenerowany stały ciąg bajtów z wklejonym w środku kluczem publicznym pasującym do adresu odbiorcy środków:

public class ScriptPubKey
{
    const byte OP_DUP = 0x76;
    const byte OP_HASH160 = 0xa9;
    const byte PUSHDATA14 = 0x14;
    const byte OP_EQUALVERIFY = 0x88;
    const byte OP_CHECKSIG = 0xac;

    private readonly byte[] _scriptPubKey;

    public int Length => GetBytesCount();

    public ScriptPubKey(string publicKey160HashHex)
    {
        _scriptPubKey =
            new[] { OP_DUP, OP_HASH160, PUSHDATA14 }
                .Concat(
                    StringHelper.HexStringToByteArray(publicKey160HashHex)
                 )
                .Concat(new[] { OP_EQUALVERIFY, OP_CHECKSIG })
                .ToArray();
    }

    public byte[] ToBytes()
    {
        return _scriptPubKey;
    }

    private int GetBytesCount()
    {
        return ToBytes().Length;
    }
}

Czas się podpisać

Do podpisywania transakcji używamy klucza prywatnego, z którego powstał nasz adres Bitcoin. Cały proces ogranicza się do obliczenia ciągu bajtów reprezentującego transakcję, wyliczenie z niego hasha, a więc podwójnie zastosowanego na nim skrótu SHA-256, a następnie podpisanie hasha kluczem prywatnym przy użyciu algorytmu ECDSA. Kolejnym etapem jest wstawienie wyniku do skryptu środków wchodzących do transakcji. Dla uproszczenia procesu przyjmujemy, że do transakcji wchodzą środki z jednego źródła.

public class TxMessage
{
    //...
	
    public void Sign(byte[] privateKey)
    {
        var publicKey = 
		    BitcoinHelper.CreatePublicKeyFromPrivate(privateKey);

        var transactionHash = ComputeHashBeforeSigning();

        var signature = 
		    BitcoinHelper.Sign(transactionHash, privateKey);

        TxIns.First().CreateSignatureScript(signature, publicKey);
    }

    private byte[] ComputeHashBeforeSigning()
    {
        var SIGHASH_ALL = new byte[] { 0x01, 0x00, 0x00, 0x00 };

        var payload = GetPayload();
        var extendedPayload = payload.Concat(SIGHASH_ALL).ToArray();

        var hash = BitcoinHelper.CalculateHash(extendedPayload);

        return hash;
    }
	
    //...
}

W powyższym procesie uwagi wymagają jeszcze dwa elementy. Pierwszy to magiczne SIGHASH_ALL. Jest to standardowo używany ciąg bajtów oznaczający, że hash został wyliczony z reprezentacji bajtowej całej transakcji. No i tutaj narzuca się drugie pytanie. Co znajduje się w miejscu podpisu w momencie liczenia skrótu, skoro wstawiamy go tam dopiero po wyliczeniu owego podpisu? Będzie to widoczne w przykładzie w dalszej części wpisu. Przed podpisaniem transakcji znajduje się tam bowiem tymczasowo kopia skryptu z części środków wychodzących poprzedniej transakcji. Takie to zagmatwane :). Proces podpisywania załatwia nam funkcjonalność biblioteki BouncyCastle:

public static class BitcoinHelper
{
    //...

    public static byte[] Sign(byte[] bytes, byte[] privateKey)
    {
        var x9EcParameters = SecNamedCurves.GetByName("secp256k1");
        var ecParams = 
            new ECDomainParameters(
                x9EcParameters.Curve, 
                x9EcParameters.G, 
                x9EcParameters.N, 
                x9EcParameters.H);

        var privateKeyBigInteger = 
            new BigInteger(
			    (new byte[] { 0x00 }).Concat(privateKey).ToArray());

        var signer = new ECDsaSigner();

        var privateKeyParameters = 
            new ECPrivateKeyParameters(privateKeyBigInteger, ecParams);

        signer.Init(true, privateKeyParameters);

        var signature = signer.GenerateSignature(bytes);

        var memoryStream = new MemoryStream();

        var sequenceGenerator = new DerSequenceGenerator(memoryStream);
        sequenceGenerator.AddObject(new DerInteger(signature[0]));
        sequenceGenerator.AddObject(new DerInteger(signature[1]));
        sequenceGenerator.Close();

        var signingResult = memoryStream.ToArray();

        return signingResult;
    }
    
    //...
}

Czas utworzyć transakcję

Dla uproszczenia procesu tworzenia transakcji zaimplementowałem metodę statyczną, która tworzy cały obiekt na podstawie już zrozumiałych parametrów. Do wykonania transakcji na potrzeby wpisu użyłem następujących parametrów:

Identyfikator poprzedniej transakcji:
c9bc2a9fc27b4c188891b94a27878257c51c1d8629ccb6ae5ebc2f0a51ce2b83
Adres nadawcy:
1Khro6tJBWbH1fPTLcnbmvuKiXgXhjVxUM
Klucz prywatny nadawcy:
0ec64a44d9d9e0cb0562b7bef26158d7215f235bf08bf18b67ba7b5bbe73028c
Adres odbiorcy:
12oWsmqvpfvQvHe4SucqUCbs5EfGm99bgm
Kwota transakcji:
0.000500

public class TxMessage
{
    //...
    
    public static TxMessage Create(
        string previousTransactionIdHex,
        string senderBitcoinAddress,
        string senderPrivateKeyHex,
        string recipientBitcoinAddress,
        double transactionAmountInBtc)
    {
        var txMessage = new TxMessage();

        var senderPrivateKey = 
            StringHelper.HexStringToByteArray(senderPrivateKeyHex);

        var scriptPubKeyPreviousTransaction = new ScriptPubKey(
            BitcoinHelper.
                Create160BitPublicKeyFromBitcoinAddress(
                    senderBitcoinAddress)
        );

        var scriptPubKeyCurrentTransaction = new ScriptPubKey(
            BitcoinHelper.
                Create160BitPublicKeyFromBitcoinAddress(
                    recipientBitcoinAddress)
        );

        var txInOutPoint = new OutPoint(
            StringHelper.HexStringToByteArray(previousTransactionIdHex), 
            0);

        txMessage.TxInsCount = new VarInt(1);
        txMessage.TxIns = new List<TxIn>
        {
            new TxIn(txInOutPoint, 
                scriptPubKeyPreviousTransaction.ToBytes())
        };

        txMessage.TxOutsCount = new VarInt(1);
        txMessage.TxOuts = new List<TxOut>
        {
            new TxOut(
                Convert.ToUInt64(transactionAmountInBtc*100000000),
                scriptPubKeyCurrentTransaction
            )
        };

        txMessage.Sign(senderPrivateKey);

        return txMessage;
    }
}

Powyższy kod służy jedynie utworzeniu pustej transakcji, wygenerowaniu skryptów, wypełnieniu odpowiednimi wartościami podanymi jako parametr i na samym końcu podpisaniu.

Czas wysłać to w świat

W jednym z początkowych wpisów z cyklu utworzyliśmy pętlę, która łączy się z siecią i odbiera wiadomości przesyłane do nas przez węzeł, z którymi nawiązaliśmy połączenie. W poniższym kodzie jedynie rozszerzyłem tę funkcjonalność o wysłanie utworzonej transakcji zaraz po połączeniu z siecią Bitcoina:

class Program
{
    //...

    public static void PerformTransaction(
        byte[] senderIpAddress,
        byte[] recipientIpAddress,
        string previousTransactionIdHex,
        string senderBitcoinAddress,
        string senderPrivateKeyHex,
        string recipientBitcoinAddress,
        double transactionAmountInBtc)
    {
        var senderShortNetworkAddress =
            new ShortNetworkAddress(senderIpAddress, 8333);

        var recipientShortNetworkAddress =
            new ShortNetworkAddress(recipientIpAddress, 8333);

        var versionMessage =
            new VersionMessage(
                recipientShortNetworkAddress, senderShortNetworkAddress);

        var transactionMessage = TxMessage.Create(
                previousTransactionIdHex,
                senderBitcoinAddress,
                senderPrivateKeyHex,
                recipientBitcoinAddress,
                transactionAmountInBtc);

        var tcpClient = 
            new TcpClient(
                recipientShortNetworkAddress.IpString, 
                recipientShortNetworkAddress.Port);
                
        var tcpClientStream = tcpClient.GetStream();

        var versionMessageBytes = versionMessage.ToBytes();
        var transactionMessageBytes = transactionMessage.ToBytes();

        tcpClientStream.Write(
            versionMessageBytes, 0, versionMessageBytes.Length);
            
        tcpClientStream.Write(
            transactionMessageBytes, 0, transactionMessageBytes.Length);

        var buffer = new byte[0];

        while (true)
        {
            var bytesToReadCount = tcpClient.Available;

            if (bytesToReadCount == 0)
            {
                Thread.Sleep(100);
                continue;
            }

            var readedData = new byte[bytesToReadCount];
            tcpClientStream.Read(readedData, 0, bytesToReadCount);

            buffer = buffer.Concat(readedData).ToArray();

            var header = new Header(buffer);

            Console.WriteLine(
              $"{DateTime.Now.ToLongTimeString()}: " +
              $"incoming message: {header.Command}. " +
              $"Length: {header.PayloadLength}");

            buffer = buffer.Skip(24 + (int)header.PayloadLength).ToArray();
        }
    }
}

Efekt końcowy!

Uruchomienie powyższego kodu zakończyło się sukcesem, a po kilku godzinach transakcja została potwierdzona:

6dad029f4baa31651f7469c30814977b96746cfb752bff2b82082c698623fa4b

Zachęcam do zagłębiania wiedzy na temat tego jak Bitcoin, czy inne waluty działają na niskim poziomie. W cyklu, który tutaj powstał, zeszliśmy jak najniżej się da – dotykając każdego poszczególnego bitu odpowiedzialnego za połączenie z siecią i wysłaniu najprostszej z możliwych transakcji w sieć. A to zaledwie ułamek całego protokołu tej kryptowaluty, która cały czas się rozwija…

Gotowa do uruchomienia solucja zawierająca kod przedstawiony w tym wpisie umieściłem na GitHubie. Zachęcam do pobrania i zabawy protokołem Bitcoina 🙂

P.S. Klucz prywatny konta, na które został wykonany przelew powstał z wyniku operacji SHA256(“lp.zsuelokim”)


Źródła, z których korzystałem przygotowując wpis:
1. https://bitcoin.org/en/developer-documentation
2. https://en.bitcoin.it/wiki/Protocol_documentation

Niniejszy post jest częścią cyklu “Protokół Bitcoina od środka”. Jeżeli zainteresowała Ciebie ta tematyka, zachęcam do zapoznania się również z pozostałymi wpisami:

  1. Zanim powiemy Hello World
  2. Budujemy nagłówek
  3. Łączymy się z siecią
  4. Generujemy adresy kont
  5. Koncepcja transakcji
  6. Jak zdobyć Bitcoiny?
  7. Budowa wiadomości „tx”
  8. Jak autoryzowane są transakcje?
  9. Ogłaszamy transakcję