Protokół blockchain cz. 1

8

W poprzednim wpisie przybliżyłem technologię blockchain. Zbudowałem prosty łańcuch bloków i przedstawiłem podstawowe elementy, z których się składa. W tym artykule chciałbym przedstawić protokół blockchain i jak on może wyglądać na podstawie krótkiego opisu i implementacji.

Ten wpis będzie pierwszą częścią dwuczęściowego artykułu poświęconego protokołowi. Ten fragment skupia się wokół tematu komunikacji między węzłami oraz algorytmu konsensusu.

Teoria

Żeby zrozumieć ten temat, musimy wprowadzić kilka dodatkowych pojęć:

  • protokół – zbiór zasad, za pomocą których różne urządzenia mogą się komunikować między sobą,
  • decentralizacja – brak centralnej jednostki nadzorującej całym procesem komunikacji między węzłami,
  • algorytm konsensusu (zgodności) – rozwiązanie problemu uzgadniania przez zbiór jednostek jednej wartości spośród zbioru wartości zaproponowanych wstępnie przez te jednostki.

Protokół w świecie blockchain to zbiór zasad, za pomocą których węzły komunikują się ze sobą. Inaczej mówiąc, definiuje reguły, dzięki którym węzły mogą stać się częścią sieci tworzonej z wykorzystaniem tej technologii. Każdy projekt bazujący na metodyce blockchain może działać, wykorzystując inny protokół.

Decentralizacja i algorytm konsensusu

Głównymi cechami, które przyczyniły się do spopularyzowania technologii blockchain, są decentralizacja i bezpieczeństwo. Żeby system był zdecentralizowany, sieć musi składać się z co najmniej dwóch węzłów, z których żaden nie jest nadrzędny. Takie urządzenia w celu zachowania równej ważności muszą być zgodne, co wiąże się zwykle z przechowywaniem identycznego łańcucha.

W tym celu istnieje zbiór reguł, za pomocą których zdecentralizowana sieć dochodzi do porozumienia w takich kwestiach jak np. aktualna wartość łańcucha bloków. Taki zbiór reguł to algorytm konsensusu.

Istnieje kilka popularnych algorytmów. Są to m.in.:

Proof of Work (dowód pracy)

W tym algorytmie użytkownicy sieci rozwiązują zagadkę w celu utworzenia bloku.

Węzeł, który pierwszy utworzy kolejny blok, będzie miał najdłuższy łańcuch, a co za tym idzie, wygra w procesie rozwiązywania konfliktu dotyczącego tego, który łańcuch jest autorytatywny, ponieważ za taki uznawany jest ten najdłuższy,

Proof of Stake (dowód posiadania)

W tym algorytmie bloki tworzone są przez tych użytkowników, którzy posiadają tokeny lub monety.

Jeśli pojawia się konflikt, to węzły głosują za pomocą swoich tokenów na odpowiedni łańcuch. Jeżeli dany użytkownik zagłosował na łańcuch, który nie wygrał głosowania, to tokeny wykorzystane przez niego do oddania głosu zostają mu zabrane,

Delegated Proof of Stake (delegowany dowód posiadania)

W tym algorytmie bloki tworzone są przez delegowane węzły wybierane przez użytkowników sieci za pomocą głosowania.

Ze względu na niewielką liczbę węzłów tworzących bloki system jest w stanie zaplanować dla każdego z delegatów odpowiednie przedziały czasowe na publikację bloków. Wybrane węzły mogą być zmieniane przez system w drodze głosowania, jeśli zachodzi ku temu potrzeba, np. delegowany węzeł nie wywiązuje się poprawnie ze swoich obowiązków. W tym modelu węzły współpracują ze sobą, zamiast rywalizować jak w modelach PoW i PoS,

Proof of Authority (dowód władzy)

W tym algorytmie bloki są tworzone przez węzły, których konta zostały poddane pozytywnej walidacji. Wspomniane elementy sieci rozwiązują konflikty i decydują o stanie systemu. Model taki wykorzystuje się zazwyczaj w prywatnych sieciach z uwagi na pewną centralizację, która w nich występuje.

Jestem pewny, że w nowych projektach wykorzystujących tę technologię powstały lub powstają obecnie różne wariacje i kombinacje korzystające z wymienionych algorytmów albo tworzone są zupełnie nowe koncepcje.

W dalszej części i w przykładowej implementacji skupiłem się na chyba najbardziej popularnym obecnie algorytmie Proof of Work.

Proof of Work, czyli kopanie, koparki i górnicy

Tak jak pisałem wcześniej, zdecentralizowana sieć składa się z co najmniej dwóch punktów. Właścicieli węzłów wchodzących w skład takiej sieci nazywa się potocznie górnikami, ponieważ muszą oni zazwyczaj wykonać jakąś pracę w celu wykopania kolejnego bloku. Cały proces kopania zachodzi na urządzeniu zwanym koparką, praca wykonywana przez górników za pomocą takiego sprzętu nazywa się potocznie kopaniem, a cały proces wydobyciem, ponieważ w pewnym stopniu te dwie czynności są do siebie podobne.

Zarówno górnicy, jak i właściciele węzłów wykonują pracę za pomocą odpowiednich narzędzi w celu uzyskania pewnego dobra. Inaczej mówiąc, zamieniają pracę na materiał, który uzyskuje wartość przez to, że dana jednostka czasu lub surowca została zużyta w celu jego otrzymania. Dlatego wirtualne jednostki powstałe w wyniku tego procesu, czyli kryptowaluty lub tokeny, mogą mieć wartość i mogą być wymieniane na inne dobra, tak jak to obecnie ma miejsce na różnych giełdach.

Przykładowy proces tworzenia bloku z wykorzystaniem prostego protokołu

Jak przebiega tworzenie bloku z wykorzystaniem prostego protokołu?

  1. Uruchomienie węzła – uruchomienie odpowiedniego programu umożliwiającego komunikację z innymi punktami sieci,
  2. rejestracja węzła – wysłanie do pozostałych węzłów sieci informacji o nowym węźle,
  3. dodawanie transakcji,
  4. dodawanie bloku (kopanie),
  5. rozwiązywanie konfliktów (uzyskiwanie konsensusu wśród węzłów).

Poniżej opisałem prostą implementację takiego protokołu.

Implementacja

Implementacja poniższego przykładu została oparta na moim poprzednim programie umieszczonym tutaj, który powstał na potrzeby poprzedniego artykułu – znajdziecie go tutaj. Są tam opisane podstawowe obiekty wchodzące w skład łańcucha bloków. Zachęcam do zapoznania się z tamtym materiałem przed przystąpieniem do poniższego.

Założenia

  • Węzły komunikują się ze sobą za pomocą protokołu HTTP w modelu klient–serwer, gdzie każdy może być klientem, a serwerem jest węzeł, na którym uruchomiony jest serwer web, zaś jego url jest zarejestrowany na pozostałych węzłach.
  • Algorytm konsensusu oparty jest na modelu Proof of Work.

Celowo nie skupiałem się na szyfrowaniu, walidacji transakcji i bloków oraz dystrybucji transakcji między węzłami. Nie ma też w poniższej implementacji kwantu czasowego, od którego uzależnione byłoby tworzenie bloku.

Tym zagadnieniom będzie poświęcona kolejna część artykułu dotyczącego protokołu blockchain. W poniższym przykładzie położyłem nacisk przede wszystkim na zapewnienie komunikacji między punktami sieci i na algorytm konsensusu.

Budowanie łańcucha bloków

Musiałem zmodyfikować nieco klasę bloku pod kątem algorytmów, jakie wykorzystałem w swoim protokole. Obecnie klasa bloku składa się z następujących właściwości:

  • indeksu – numeru bloku (liczby porządkowej),
  • stempla czasowego – czasu utworzenia bloku,
  • nonce – dowodu pracy (rozwiązania zagadki),
  • hasza poprzedniego bloku,
  • listy transakcji.
public class Block
{
    public int Index { get; set; }
    public DateTime Timestamp { get; set; }
    public int Nonce { get; set; }
    public string PreviousHash { get; set; }
    public List<Transaction> TransactionList { get; set; }
}

Następnie utworzyłem klasę Blockchain, która składa się z następujących właściwości:

  • identyfikatora węzła – adresu użytkownika, który może być wykorzystywany do przesyłania jednostek rozliczeniowych,
  • pustej listy aktualnych transakcji – do tej listy są dodawane nowe transakcje, które są umieszczane w nowym bloku po jego wykopaniu,
  • pustej listy bloków – ta lista będzie zawierała wszystkie sprawdzone i zatwierdzone bloki, tworząc łańcuch bloków,
  • listy węzłów sieci – listy wszystkich węzłów, które zostały zarejestrowane w sieci.
public class Blockchain
{
    public string NodeId { get; }
    private readonly List<Transaction> currentTransactionList = new List<Transaction>();
    private List<Block> blockList = new List<Block>();
    private readonly List<Node> nodes = new List<Node>();
}

Łańcuch bloków po zainicjalizowaniu wygląda tak:

{  
   "blockList":[  
      {  
         "Index":0,
         "Timestamp":"2018-11-01T10:52:11.7555162Z",
         "Nonce":100,
         "PreviousHash":"",
         "TransactionList":[]
      }
   ],
   "length":1
}

Tworzenie nowych bloków

Podczas inicjalizowania łańcucha bloków tworzony jest bazowy blok, tzw. genesis block. Jest to blok, który nie ma przodków (poprzednich bloków), hasz wcześniejszego bloku jest pusty, a dowód pracy – ustawiony na stałą wartość (nie jest obliczany).

Uzupełniłem klasę Blockchain o dodatkowe metody dołączania nowego bloku CreateNewBlock, dodawania transakcji CreateTransaction oraz generowania hasza bloku GetHash:

private Block CreateNewBlock(int nonce, string previousHash = null)
{
    var block = new Block(this.blockList.Count, DateTime.UtcNow, nonce,
                          previousHash ?? this.GetHash(this.blockList.Last()),
                          this.currentTransactionList.ToList());

    this.currentTransactionList.Clear();
    this.blockList.Add(block);
    return block;
}

internal int CreateTransaction(string from, string to, double amount)
{
    var transaction = new Transaction
    {
        From = from,
        To = to,
        Amount = amount
    };

    this.currentTransactionList.Add(transaction);
    return this.LastBlock?.Index + 1 ?? 0;
}

private string GetHash(Block block)
{
    string blockText = JsonConvert.SerializeObject(block);
    return Helper.GetSha256Hash(blockText);
}

Tworzenie bloku odbywa się podczas procesu kopania (metoda klasy Blockchain):

internal string Mine()
{
    int nonce = this.FindNonce(this.LastBlock.Nonce, this.LastBlock.PreviousHash);

    this.CreateTransaction("0", this.NodeId, 1);
    Block block = this.CreateNewBlock(nonce /*, _lastBlock.PreviousHash*/);
}

W procesie kopania następuje praca polegająca na poszukiwaniu dowodu, tzw. nonce. W omawianym przykładzie przebiega to w pętli, w której nonce, począwszy od wartości 0, jest sprawdzany pod kątem poprawności. Jeśli jest poprawny, to następuje wyjście z pętli, a jeśli nie, to nonce zwiększany jest o 1 i operacja w pętli się powtarza.

Sprawdzanie poprawności polega na generowaniu ciągu znaków składającego się z ostatniego dowodu pracy (lastNonce), aktualnie sprawdzanego dowodu (nonce) i hasza poprzedniego bloku (previousHash). Następnie taki ciąg jest haszowany i sprawdzane jest, czy początek hasza zaczyna się od trzech zer. Po znalezieniu odpowiedniego dowodu dodawana jest do nowego bloku transakcja o wartości 1 jednostki rozliczeniowej, której odbiorcą jest bieżący węzeł. Jest to nagroda za wykopanie bloku.

private int FindNonce(int lastNonce, string previousHash)
{
    int nonce = 0;
    while (!this.IsValidNonce(lastNonce, nonce, previousHash))
        nonce++;

    return nonce;
}

private bool IsValidNonce(int lastNonce, int nonce, string previousHash)
{
    string guess = $"{lastNonce}{nonce}{previousHash}";
    string result = Helper.GetSha256Hash(guess);
    return result.StartsWith("000");
}

Algorytm konsensusu – rozwiązywanie konfliktów

Algorytm porozumienia polega na tym, że każdy z węzłów pobiera łańcuch bloków od każdego innego węzła zarejestrowanego w sieci i porównuje długość swojego łańcucha z tym pobranym. Jeśli długość pobranego łańcucha jest dłuższa, to łańcuch w źródłowym węźle jest nadpisywany. Chodzi o to, że węzeł o najdłuższym łańcuchu jako pierwszy znalazł dowód pracy i jego blok został dopisany do łańcucha.

private bool ResolveConflicts()
{
    List<Block> newChain = null;

    foreach (Node node in this.nodes)
    {
        var url = new Uri(node.Address, "/blockchain");
        var request = (HttpWebRequest) WebRequest.Create(url);
        var response = (HttpWebResponse) request.GetResponse();

        if (response.StatusCode == HttpStatusCode.OK)
        {
            var model = new
            {
                blockList = new List<Block>(),
                length = 0
            };
            Stream stream = response.GetResponseStream();
            if (stream != null)
            {
                string json = new StreamReader(stream).ReadToEnd();
                var data = JsonConvert.DeserializeAnonymousType(json, model);

                if (data.blockList.Count > this.blockList.Count &amp;&amp; this.IsValidBlockList(data.blockList))
                {
                    newChain = data.blockList;
                }
            }
            else
            {
                Debug.WriteLine("Unable to get response stream");
            }
        }
    }

    if (newChain != null)
    {
        this.blockList = newChain;
        return true;
    }

    return false;
}

Serwer web

Do implementacji serwera web, za pomocą którego węzły komunikują się ze sobą, została wykorzystana prosta biblioteka TinyWebServer. Za jej pomocą możliwe jest odbieranie i przetwarzanie żądań. Na tym serwerze przygotowałem kilka metod. Służą one do wykonywania odpowiednich operacji na łańcuchu bloków danego węzła:

  • mine – kopanie – tworzenie nowego bloku,
  • transactions/new – dodawanie nowej transakcji,
  • blockchain – pobieranie łańcucha bloków,
  • nodes/register – rejestracja węzłów,
  • nodes/resolve – rozwiązywanie konfliktów.

Do uruchomienia serwera została napisana prosta aplikacja konsolowa. Tworzy ona nowy obiekt Blockchain i na jego podstawie inicjalizuje serwer web, który zaczyna nasłuchiwać na danym adresie i porcie ustawionym w pliku konfiguracyjnym appsettings.json.

static void Main(string[] args)
{
    IConfiguration config = Helper.GetConfigFromFile("appsettings.json");

    var blockchain = new Blockchain.Blockchain();
    var unused = new WebServer(blockchain, config["server"], config["port"]);
    Console.WriteLine($"Serwer o adresie {config["server"]}:{config["port"]} został uruchomiony");
    Console.Read();
}

Uruchomienie i test systemu

Do testu przygotowałem dwie wersje aplikacji konsolowej różniące się plikiem konfiguracyjnym, a następnie uruchomiłem je.

PS C:\> dotnet C:\#Temp\publish1\MySimpleBlockchainWithPoW.ServerCore.dll
Serwer o adresie localhost:12345 został uruchomiony
PS C:\> dotnet C:\#Temp\publish2\MySimpleBlockchainWithPoW.ServerCore.dll
Serwer o adresie localhost:54321 został uruchomiony

Dzięki temu pojawiły się dwa węzły – jeden pod adresem localhost:12345 (numer f0f673698fa04d10bd3be1c457a2f9b6), a drugi pod adresem localhost:54321 (numer 54dd864a5b3444b3b30ed07568163629).

Następnie używając aplikacji Postman, wysyłam żądania do węzłów. Żeby połączyć te dwie końcówki w sieć, muszę je wzajemnie zarejestrować, wysyłając żądania nodes/register:

REQUEST POST url: http://localhost:54321/nodes/register body: {"url": "localhost:12345"}
REQUEST POST url: http://localhost:12345/nodes/register body: {"url": "localhost:54321"}

W rezultacie otrzymują odpowiedź odpowiednio:

RESPONSE body: "Węzeł localhost:12345 został zarejestrowany"
RESPONSE body: "Węzeł localhost:54321 został zarejestrowany"

Kopię pierwszy blok (pusty):

REQUEST GET url: http://localhost:54321/mine

RESPONSE body:

{
    "Message": "Nowy blok został wygenerowany",
    "Index": 1,
    "Transactions": [
        {
            "Amount": 1.0,
            "From": "0",
            "To": "54dd864a5b3444b3b30ed07568163629"
        }
    ],
    "Nonce": 862,
    "PreviousHash": "515a53f26e8047ff2cfe84ca5a632dfb56c05099c2096a5d7cfa3801839bfb48"
}

Następnie wykonuję synchronizację bloków:

REQUEST GET url: http://localhost:12345/nodes/resolve
REQUEST GET url: http://localhost:54321/nodes/resolve

Po tej transakcji węzeł o numerze 54dd864a5b3444b3b30ed07568163629 posiada 1 jednostkę i łańcuchy są zsynchronizowane. W kolejnym kroku dodaję transakcję:

REQUEST POST url: http://localhost:54321/transactions/new body: { "Amount":0.5, "From":"54dd864a5b3444b3b30ed07568163629", "To":"f0f673698fa04d10bd3be1c457a2f9b6" }
RESPONSE body: Twoja transakcja zostanie dodana do bloku 2

Kopię kolejny blok:

REQUEST GET url: http://localhost:54321/mine

RESPONSE body:

{
    "Message": "Nowy blok został wygenerowany",
    "Index": 2,
    "Transactions": [
        {
            "Amount": 0.5,
            "From": "54dd864a5b3444b3b30ed07568163629",
            "To": "f0f673698fa04d10bd3be1c457a2f9b6"
        },
        {
            "Amount": 1.0,
            "From": "0",
            "To": "54dd864a5b3444b3b30ed07568163629"
        }
    ],
    "Nonce": 1593,
    "PreviousHash": "1e582c742100802ee887497ad69c6a69abe520ce57d0061aeaeebf8987c7bf3e"
}

Na koniec znowu wykonuję synchronizację bloków:

REQUEST GET url: http://localhost:54321/nodes/resolve

RESPONSE body:

{
    "Message": "Nasz blockchain jest autorytatywny",
    "BlockList": [
        {
            "Index": 0,
            "Timestamp": "2018-11-12T10:02:55.7210083Z",
            "Nonce": 100,
            "PreviousHash": "",
            "TransactionList": []
        },
        {
            "Index": 1,
            "Timestamp": "2018-11-12T10:11:18.5521277Z",
            "Nonce": 862,
            "PreviousHash": "515a53f26e8047ff2cfe84ca5a632dfb56c05099c2096a5d7cfa3801839bfb48",
            "TransactionList": [
                {
                    "Amount": 1.0,
                    "From": "0",
                    "To": "54dd864a5b3444b3b30ed07568163629"
                }
            ]
        },
        {
            "Index": 2,
            "Timestamp": "2018-11-12T10:33:20.1480628Z",
            "Nonce": 1593,
            "PreviousHash": "1e582c742100802ee887497ad69c6a69abe520ce57d0061aeaeebf8987c7bf3e",
            "TransactionList": [
                {
                    "Amount": 0.5,
                    "From": "54dd864a5b3444b3b30ed07568163629",
                    "To": "f0f673698fa04d10bd3be1c457a2f9b6"
                },
                {
                    "Amount": 1.0,
                    "From": "0",
                    "To": "54dd864a5b3444b3b30ed07568163629"
                }
            ]
        }
    ]
}
REQUEST GET url: http://localhost:12345/nodes/resolve

RESPONSE body:

{
    "Message": "Nasz blockchain został zamieniony",
    "BlockList": [
        {
            "Index": 0,
            "Timestamp": "2018-11-12T10:02:55.7210083Z",
            "Nonce": 100,
            "PreviousHash": "",
            "TransactionList": []
        },
        {
            "Index": 1,
            "Timestamp": "2018-11-12T10:11:18.5521277Z",
            "Nonce": 862,
            "PreviousHash": "515a53f26e8047ff2cfe84ca5a632dfb56c05099c2096a5d7cfa3801839bfb48",
            "TransactionList": [
                {
                    "Amount": 1.0,
                    "From": "0",
                    "To": "54dd864a5b3444b3b30ed07568163629"
                }
            ]
        },
        {
            "Index": 2,
            "Timestamp": "2018-11-12T10:33:20.1480628Z",
            "Nonce": 1593,
            "PreviousHash": "1e582c742100802ee887497ad69c6a69abe520ce57d0061aeaeebf8987c7bf3e",
            "TransactionList": [
                {
                    "Amount": 0.5,
                    "From": "54dd864a5b3444b3b30ed07568163629",
                    "To": "f0f673698fa04d10bd3be1c457a2f9b6"
                },
                {
                    "Amount": 1.0,
                    "From": "0",
                    "To": "54dd864a5b3444b3b30ed07568163629"
                }
            ]
        }
    ]
}

Jako że kopaliśmy na węźle localhost:54321, to tam był dłuższy łańcuch i dlatego on był autorytatywny, a na końcówce localhost:12345 został zamieniony. Jak widać, po rozwiązaniu konfliktów oba łańcuchy są identyczne i zawierają transakcje, które dodałem.

Źródła do tego przykładu są dostępne na GitHubie: MySimpleBlockchainWithPoW.

Podsumowanie

W tym tekście teoria i implementacja protokołu blockchain była skupiona wokół komunikacji między węzłami i algorytmu konsensusu.

Przedstawia wykorzystanie podstawowych algorytmów i mechanizmów składających się na protokół, które są obecnie używane w praktycznym zastosowaniu technologii blockchain. Są zachowane najważniejsze cechy metodyki, czyli niezmienność i decentralizacja.

W kolejnej części dodam jeszcze dodatkowe mechanizmy takie jak walidacja, szyfrowanie i synchronizacja transakcji opartych na algorytmach klucza prywatnego. Wprowadzę określone fragmenty czasu do tworzenia bloków.

Daj znać w komentarzu: co o tym sądzisz? O czym chcesz się jeszcze dowiedzieć?

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Share.

About Author

Nazywam się Paweł i pracuję jako menedżer projektów informatycznych i developer. Kocham programowanie i nowe technologie. Dlatego też technologia blockchain tak bardzo mnie zainteresowała. Lubię czytać i uczyć się nowych rzeczy które mają zastosowanie w codziennym życiu. Mieszkam w Ustroniu, gdzie pracuję - głównie zdalnie. Kiedy nie programuję albo nie zarządzam projektami troszczę się o moją żonę i dzieci - mam ich dwoje, 8 letniego chłopca i 5 letnią dziewczynkę. Oprócz tego bardzo lubię sport - ostatnio praktykuję głównie siłownię i bieganie.

8 Comments

  1. Jeszcze nie zapoznałem się dokładnie z treścią ale po poprzednim artykule i szybkim przejrzeniu obecnego, już nie mogę się doczekać aż do tego siądę :)

  2. Mnie zastanawiała zawsze jedna rzecz – mianowicie mówi się, że zaletą blockchaina jest decentralizacja i wymiana p2p; że niby każdy z każdym jest połączony i może się komunikować. Ok, ale stawiając serwer w domu za ruterem jestem praktycznie odcięty od świata, więc taki serwer jest nic nie warty.. Aby wszystko działało poprawnie muszę posiadać adres publiczny (lub ewentualnie przekierowania portów na ruterze) i wtedy będę mógł się łączyć do serwerów, które również posiadają publiczne IP, czyli defacto nie każdy z każdym, a tylko z ‘głównymi’ serwerami.. Dobrze to rozumiem?

    • W każdym projekcie to może być rozwiązane inaczej. Wszystko właśnie zależy od wykorzystanego protokołu. Jeśli mówimy o projekcie na wzór bitcoin-a gdzie algorytmem konsensusu jest PoW to żeby być węzłem sieci musimy mieć możliwość połączenia do pozostałych węzłów jak i inne węzły muszą mieć możliwość połączenia z nami. Tak więc każdy z każdym. Być może mylisz tutaj dwie rzeczy – bycie węzłem a bycie użytkownikiem. Można korzystać z sieci np. wykonać transakcję nie będąc węzłem i wtedy wystarczy że tylko my mamy połączenie z jakimś węzłem w celu wykonania jakiejś operacji.