Cześć! Właśnie czytasz bloga, na którym znajdziesz trochę informacji o Bitcoinie, trochę o .Net i trochę o tym co przyjdzie mi do głowy :)
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:
[code language=”csharp” title=”VarInt.cs”]
public class VarInt
{
public UInt64 Value { get; }
public VarInt(UInt64 value)
{
Value = value;
}
public VarInt(byte[] bytes)
{
switch (bytes[0])
{
case 0xFF:
Value = Convert.ToUInt64(BitConverter.ToUInt64(bytes, 1));
break;
case 0xFE:
Value = Convert.ToUInt64(BitConverter.ToUInt32(bytes, 1));
break;
case 0xFD:
Value = Convert.ToUInt64(BitConverter.ToUInt16(bytes, 1));
break;
default:
Value = Convert.ToUInt64(bytes[0]);
break;
}
}
public byte[] ToBytes()
{
var bytes = new byte[1];
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:
Pole to zawiera informacje na temat dodatkowych funkcji, które aplikacja może obsłużyć. W tym momencie pole to może przyjąć tylko wartości 1 lub 0. 1 oznacza, że aplikacja posiada pełne bloki zawierające historię transakcji sieci i inne aplikacje mogą o te bloki wysłać zapytanie. 0 oznacza, że aplikacja zawiera jedynie nagłówki wspomnianych bloków. Przyjmijmy, że będziemy tę wartość ustawiać zawsze na 1.
Numer pola
2
Nazwa pola
IP address
Liczba bajtów
16
Pole reprezentujące adres IP. Może to być adres IPv4 jak i IPv6. W naszej implementacji przyjmiemy, że korzystamy tylko z adresów IPv4. W takim przypadku pole to dalej zajmuje 16 bajtów. Ostatnie 4 bajty wyznaczają właściwy adres, 11-sty i 12-sty bajt mają wartość 0xFF, zaś reszta pozostaje zerami.
Numer pola
3
Nazwa pola
port
Liczba bajtów
2
Numer portu na jakim odbywa się komunikacja z węzłem. Żeby nie było zbyt prosto – liczba reprezentująca port powinna być w formacie big endian. Dlatego przy konwersji bajtów na reprezentację liczbową będziemy musieli zamienić ich kolejność.
Implementacja klasy ShortNetworkAddress prezentuje się następująco:
[code language=”csharp” title=”ShortNetworkAddress.cs”]
public class ShortNetworkAddress
{
public UInt64 Services { get; }
public byte[] Ip { get; }
public UInt16 Port { get; }
public ShortNetworkAddress(byte[] ip, UInt16 port)
{
Services = 1;
Ip = ip;
Port = port;
}
public byte[] ToBytes()
{
return BitConverter.GetBytes(Services)
.Concat(GetIpBytes()).Concat(GetPortBytes()).ToArray();
}
public ShortNetworkAddress(byte[] bytes)
{
Services = BitConverter.ToUInt64(bytes, 0);
Ip = new[] { bytes[20], bytes[21], bytes[22], bytes[23] };
Port = BitConverter.ToUInt16(new[] { bytes[25], bytes[24] }, 0);
}
private byte[] GetIpBytes()
{
var result = new byte[16];
for (var i = 0; i < 10; ++i)
{
result[i] = 0;
}
result[10] = result[11] = 0xFF;
for (var i = 0; i < 4; i++)
{
result[12 + i] = Ip[i];
}
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”:
Wersja protokołu, jaką się komunikujemy. W naszym przypadku jest to 70002.
Numer pola
3
Nazwa pola
services
Liczba bajtów
8
Przyjmiemy tutaj na stałe wartość 1, która oznacza, że aplikacja posiada pełne bloki zawierające historię transakcji sieci i inne aplikacje mogą o te bloki wysłać zapytanie. Trochę tutaj oszukujemy, ale dzięki temu zobaczymy o co sieć może nas poprosić.
Numer pola
4
Nazwa pola
timestamp
Liczba bajtów
8
Aktualny czas w formacie uniksowym na komputerze wysyłającym wiadomość .
Numer pola
5
Nazwa pola
addr_recv
Liczba bajtów
26
Adres IP węzła docelowego, z którym odbywa się połączenie.
Numer pola
6
Nazwa pola
addr_from
Liczba bajtów
26
Adres IP z którego wykonujemy połączenie.
Numer pola
7
Nazwa pola
nonce
Liczba bajtów
8
Liczba losowa identyfikująca konkretny węzeł. Jeżeli wartość tego pola wynosi 0, jego znaczenie jest ignorowane. Dla uproszczenia – użyjemy w naszej aplikacji wartość 0.
Numer pola
8
Nazwa pola
user_agent
Liczba bajtów
różna
Nazwa aplikacji obsługującej protokół. Pierwsze bajty określają długość nazwy, następnie po nich występuje ciąg znaków w formacie ASCII. Nazwijmy naszą aplikację “/MikoleuszBlogBitcoinConnector:1.0/”.
Numer pola
9
Nazwa pola
start_height
Liczba bajtów
4
Numer najnowszego posiadanego bloku transakcji. W chwili powstawania tego wpisu najnowszy blok wpuszczony do sieci ma numer 400264. Taki właśnie przyjmiemy w aplikacji.
Numer pola
10
Nazwa pola
relay
Liczba bajtów
1
Pole określające, czy aplikacja chce otrzymywać informacje na temat nowych transakcji pojawiających się w sieci Bitcoin. Jasne, że chcemy – przyjmujemy wartość 1.
No i wreszcie – implementacja przedstawionej specyfikacji:
private void ComputeHeader()
{
var payload = GetPayload();
Header.CalculateChecksum(payload);
}
private byte[] GetPayload()
{
var payload =
BitConverter.GetBytes(ProtocolVersion)
.Concat(BitConverter.GetBytes(Services))
.Concat(BitConverter.GetBytes(Timestamp))
.Concat(this.RecipientAddress.ToBytes())
.Concat(this.SenderAddress.ToBytes())
.Concat(BitConverter.GetBytes(Nonce))
.Concat(UserAgentLength.ToBytes())
.Concat(Encoding.ASCII.GetBytes(UserAgent))
.Concat(BitConverter.GetBytes(StartHeight))
.Concat(Relay ? new byte[] { 0x01 } : new byte[] { 0x00 })
.ToArray();
return payload;
}
}
[/code]
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:
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:
version – wizytówka węzła docelowego – dokładnie taka, jaką do niego wysłaliśmy;
verack – akceptacja połączenia, a w zasadzie wersji protokołu, którym się posługujemy;
ping – w wolnym tłumaczeniu “Żyjesz?”. Powinniśmy odpowiedzieć na taką wiadomość “Żyję!” – przy pomocy komunikatu pong;
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;
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”;
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…
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: