Park Miniatur Zabytków Dolnego Śląska w Kowarach

Protokół Bitcoina od środka: Łączymy się z siecią

Do obecności w sieci Bitcoina wystarczy dowolny klient implementujący jego protokół oraz połączenie Internetem. A jakby tak nauczyć się jego języka i pogaworzyć przy użyciu własnej aplikacji? Zobaczymy o czym gada Bitcoin 🙂

Co już wiemy?

W poprzednich wpisach z cyklu „Protokół Bitcoina od środka” poznaliśmy teorię związaną z budową wiadomości odpowiedzialnej za zainicjowanie komunikacji z siecią Bitcoin. Do tej pory udało nam się zaimplementować nagłówek, który jest podstawą każdego komunikatu. Czas na skonstruowanie powitania i wysłania go w wirtualny świat cyfrowej waluty.

Do zbudowania wiadomości powitalnej brakuje nam jeszcze implementacji dwóch klas: VarInt reprezentującej liczbę w świecie Bitcoina oraz ShortNetworkAddress reprezentującej adres IP.

VarInt

Twórcom Bitcoina najwyraźniej dopisywał humor podczas tworzenia protokołu, bowiem zamiast reprezentować liczby całkowite przy pomocy 64-bitów – postanowili uprzykrzyć życie deweloperom wpowadzając reprezentację liczby o zmienym rozmiarze. Im większa liczba, tym więcej bajtów zajmuje – od 1-go do 9-ciu. Zakres liczb i wielkość reprezentacji w poniższej tabelce:

Czy nam się to podoba, czy nie – sieć Bitcoina z nami nie pogada, jeśli nie będziemy mówili w jej języku – implementujmy wspomniany koncept:


Dla przyszłych potrzeb – zaimlementowana została możliwość tworzenie liczby na podstawie ciągu bajtów. Okaże się pomocne, gdy będziemy odbierać pakiety od sieci z próbą ich interpretacji.

ShortNetworkAddress

Przyda nam się jeszcze klasa reprezentująca adres IP. Protokół Bitcoina przewiduje użycie adresów w formacie IPv6, jednak dla uproszczenia implementacji przyjmijmy, że zadowala nas IPv4. Budowa reprezentacji adresu IP wraz z portem, na którym odbywa się komunikacja w poniższej tabeli:


Implementacja klasy ShortNetworkAddress prezentuje się następująco:

Czas zbudować powitanie

Samo zainicjowanie połączenia z siecią przy samodzielnej implementacji protokołu wymaga trochę zabawy. Później jest już tylko prościej – do budowy kolejnych wiadomości wykorzystamy zaimplementowane do tej pory komponenty. Czas na zbudowanie wiadomości powitalnej, która sprawi, że sieć Bitcoina powie – „To jest nasz ziomek, możemy sobie pogadać”. Poniżej tabela ze specyfikacją wiadomości „version”:


No i wreszcie – implementacja przedstawionej specyfikacji:


Kilka fragmentów, które pojawiły się w powyższym kodzie wymagają komentarza:

1. W klasie pomocniczej BitcoinHelper pojawiła się metoda GetCurrentTimeStamp() zwracająca aktualny czas. Jej implementacja przedstawia się następująco:

public static class BitcoinHelper
{
  /*...*/
  public static Int32 GetCurrentTimeStamp()
  {
    return (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)))
	     .TotalSeconds;
  }
}

2. Payload to zestaw bajtów reprezentujacy wiadomość. Potrzebny jest on do wyliczenia sumy kotrolnej i wstawieniu jej w odpowiednie pole nagłówka. Połączenie bajtów nagłówka z już wyliczoną sumą kontrolną i wiadomością składa się na komplet danych, które możemy wysyłać w sieć.

3. Nowa metoda CalculateChecksum(payload) z klasy Header, która została użyta przy aktualizowaniu zawartości nagłówka wymaga doimplementowania. Jej celem jest ustawienie w nagłówku długość wiadomości oraz wyliczenie nowej sumy kontrolnej na podstawie jej zawartości. Implementacja tej metody wygląda następująco:

public class Header
{
  /*...*/
  public void CalculateChecksum(byte[] payload)
  {
    PayloadLength = Convert.ToUInt32(payload.Length);
    Checksum = BitcoinHelper.CalculateChecksum(payload);
  }
}

No to wysyłamy

Mając już przygotowany zestaw wszystkich klas potrzebnych do zainicjowania połączenia, czas przekonać się, czy ciąg bajtów, który wyplujemy jest zrozumiały w świecie Bitcoina. Napiszmy zatem prostą, konsolową aplikację, której zadaniem będzie skonstruowanie zgodnie ze sztuką wiadomości typu Version, wysłanie jej do wybranego węzła sieci Bitcoin i w nieskończonej pętli badanie, co sieć w odpowiedzi do nas przyśle. W wersji tej zajmiemy się jedynie interpretacją bajtów reprezentowanych przez nagłówek, aby stwierdzić, czy zostaliśmy zaakceptowani i czego możemy się od Bitcoina spodziewać. Po zinterpretowaniu nagłówka będziemy omijać mięso wiadomości, czyli Payload i czekać na dalsze pakiety danych. Adres losowego węzła sieci możemy zdobyć sposobem opisanym w jednym z poprzednich artykułów niniejszego cyklu.

Poniższy kod źródłowy przedstawia użycie opisanych klas do zainicjowania komunikacji z siecią Bitcoin:

static void Main()
{
  var recipientAddress = new ShortNetworkAddress(
    new byte[] { 188, 213, 171, 239 }, 8333);

  var senderAddress = new ShortNetworkAddress(
    new byte[] { 77, 9, 139, 233 }, 8333);

  var versionMessage = new Version(recipientAddress, senderAddress);

  var bytesToSend = versionMessage.ToBytes();

  var tcpClient = new TcpClient("188.213.171.239", recipientAddress.Port);

  var stream = tcpClient.GetStream();
  stream.Write(bytesToSend, 0, bytesToSend.Length);

  var buffer = new byte[0];

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

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

    var readedData = new byte[bytesToReadCount];
    stream.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();
  }
}

A taki jest efekt uruchomienia aplikacji:

Wynik wysłania wiadomości typu Version do sieci Bitcoina
Wynik wysłania wiadomości typu Version do sieci Bitcoina

Wygląda na to, że próba połączenia zakończyła się powodzeniem 🙂 Krótkiego komentarza wymaga jeszcze odpowiedź od węzła Bitcoina:

  1. version – wizytówka węzła docelowego – dokładnie taka, jaką do niego wysłaliśmy;
  2. verack – akceptacja połączenia, a w zasadzie wersji protokołu, którym się posługujemy;
  3. ping – w wolnym tłumaczeniu „Żyjesz?”. Powinniśmy odpowiedzieć na taką wiadomość „Żyję!” – przy pomocy komunikatu pong;
  4. addr – taką wiadomością węzeł ogłasza, że zna jeszcze inne adresy komputerów w sieci Bitcoin i gdybyśmy mieli ochotę się z nimi połączyć, to proszę bardzo;
  5. getheaders – ledwo, co węzeł podzielił się garstką informacji o sieci, a już żąda 🙂 Wiadomość ta oznacza nic innego jak „Masz jakieś informacje o nowych transakcjach w sieci? Prześlij mi X nagłówków transakcji zaczynając od tej z numerem Y”;
  6. inv – tą wiadomością węzeł rozgłasza informacje o konkretnych transakcjach lub ich blokach. Najczęściej są to transakcje, które właśnie miały miejsce, lecz jeszcze nie zostały potwierdzone.

Połączenie z siecią Bitcoina przy pomocy własnej aplikacji wymaga przyswojenia kilku konceptów i przelania ich w niemałą liczbę linii kodu. Jednak obsługa każdych kolejnych wiadomości wiąże się już tylko z dopisaniem niewielkich klas. Jak widać na powyższym przykładzie, nie musimy implementować całego protokołu, aby rozmawiać w języku Bitcoina. Nie musimy też implementować obsługi wszystkich wiadomości, aby wykonać w sieci prawdziwą transakcję. Ale to już historia na inny wpis…


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