No masters, no slaves, blockchain inside – pisanie zdecentralizowanych aplikacji

Blockchain idealnie wpisuje się w ostatni trend politycznej poprawności w świecie IT. Nie ma nodów master ani slave, wszyscy są równi niezależnie od płci miejsca w sieci. Framework Dinemic jest biblioteką, która pomaga tworzyć zdecentralizowane aplikacje w C++ oparte o technologię, która jest pochodną Blockchain, X509 oraz kilku innych technologi. Służy głównie do tworzenia programów, które mają działać na wielu maszynach równocześnie zapewniając przy tym wysoką wydajność i odporność na awarie.

Chociaż dinemic jest de facto ORM’em dla C++, to spróbujmy na początek inaczej. Wyobraź sobie repozytorium Git’a. Takie, w którym to nie developerzy robią commity, a poszczególne obiekty klas, a każdy commit jest informacją na temat zmiany stanu takiego obiektu (i bazy danych pod nim), podpisaną przez niego cyfrowo. Dodatkowo pomyśl, że nie mamy jednego centralnego serwera Git’a, ale każdy komputer w sieci jest za razem repozytorium lokalnym i remote’m dla innych repozytoriów. Jeszcze tylko dodać, że nieautoryzowane zmiany są odrzucane przez inne repa i mamy obraz framework’a dinemic. To tak w skrócie. Po nieco obszerniejszy opis zapraszam dalej.

Jak to zwykle działa

Zwykle aplikacje, nawet te obsługujące większe serwisy, mają następującą architekturę: program, na przykład w PHP, jest hostowany przez serwer www. Jego logika odpowiada on za przygotowanie i udostępnienie treści zapisanych w bazie danych. W ostatnim czasie, nawet jeśli jest to aplikacja desktopowa lub mobilna, to i tak jej backend jest obsługiwany przez serwis oparty o zapytania HTTP.

Jak przy każdym serwisie, podczas budowania takiej aplikacji powstaje cała masa pytań. Jedno z najczęściej spotykanych to jak autoryzować ich użytkowników? W najprostszej formie można to zrobić na poziomie bazy danych, nie komplikując zbytnio logiki swoich aplikacji. Jednak w miarę rozwoju lub chcąc integrować się z zewnętrznymi serwisami pozostaje przeniesienie całej autoryzacji użytkowników do zewnętrznego lub wydzielonego wewnętrznego (mikro)serwisu.

Na koniec pozostaje do rozwiązania problem dostępności takiej usługi, jej bezpieczeństwa i całej masy innych problemów, na które prawdopodobnie się napotka. Na przykład granulacji uprawnień: czy użytkownik może lub nie mieć dostępu do serwisu, czy też administrator może zdecydować do jakich jego części może mieć dostęp? Jeśli to drugie, to czy chcemy dawać dostęp do wszystkich funkcjonalności, czy mieć możliwość ich wyboru? I tak dalej…

Do tego może jeszcze dojść zewnętrzny system autoryzacji, np. przez google lub facebooka, i mamy gotowy projekt na długie miesiące służący jedynie do odpowiedniego “wpuszczenia” użytkownika do naszego systemu. Odpowiedniego, czyli takiego aby nie mógł za dużo nabroić i żeby nie był całkowicie sparaliżowany ograniczeniami. Do tego jeszcze należy pamiętać o zabezpieczeniu wszystkiego, odpowienio mocnych hashah haseł, sposobie hashowania, dedykowanym API… ech…


Architektura OpenStack Havana 🙂 (tak, stare)

Jak to działa z dinemic

Wyobraź sobie następujący scenariusz z Git’a (chociaż dinemic nie ma z nim nic wspólnego). Każdy developer tworzy swoje zmiany we własnych gałęziach. Czasami te zmiany zachodzą na siebie tworząc konflikty, a czasami po prostu konieczne jest zmergowanie dwóch gałęzi w jedną tak, aby połączyć nowe fragmenty kodu z gałęzią stable. Wszystkie takie zmiany są uporządkowane w logiczny ciąg lub nawet graf określający ich kolejność.

Co się dzieje gdy dwóch developerów pisze w osobnych kopiach repozytorium? Zwykle nic. I jest to bardzo częste gdy wiele osób pracuje na własnych komputerach. Po połączeniu się z repozytorium ich zmiany są nakładane na siebie, a w przypadku konfliktów każdy z nich rozwiązuje je po swojemu.

Teraz wyobraź sobie bardzo podobny scenariusz, tylko zamiast programistów mamy obiekty klas w C++, których stan zmienia się co jakiś czas. Każda taka zmiana to nowa aktualizacja wpięta w łańcuch lub graf takich aktualizacji, podobnie jak commit w Git. Każda z tych zmian jest też na bieżąco rozsyłana po sieci klastra, w którym działają aplikacje oparte o framework dinemic. W Gicie programiści są identyfikowani kluczami SSH i mogą zmieniać dowolne gałęzie. W framerowku dinemic każdy obiekt klasy podczas jego tworzenia ma generowany zestaw kluczy kryptograficznych, którymi podpisuje się pod swoimi zmianami. Może to robić tylko sam w sobie lub w obiektach, które dodały jego publiczny klucz do listy autoryzowanych obiektów.

Oprócz tego wszystkie te zmiany są porządkowane w formie łańcuchów umożliwiających odtworzenie stanu klastra z dowolnego momentu i ewentualne połączenie (merge) po awarii klastra.

Bezpieczeństwo i uprawnienia

Każdy obiekt, jaki jest tworzony przez klasy dziedziczące po DModel automatycznie generuje i zapisuje parę kluczy kryptograficznych na hoście, na którym został utworzony. Za pomocą tych kluczy będą podpisane cyfrowo wszelkie zmiany w tym obiekcie, począwszy od informacji o jego stworzeniu, przez zmiany dowolnych pól aż po żądanie skasowania z wszystkich replik w klastrze. Sekretny, prywatny klucz jest tworzony tylko tam, gdzie został stworzony obiekt. Publiczna część klucza jest rozsyłana po całym klastrze wraz z informacją, że obiekt został utworzony w rozproszonej bazie. Na publicznej części klucza opierany też jest ID obiektu, tak więc nie ma możliwości (inaczej – jest to bardzo trudne) podrobienia obiektu o tym samym ID. Jeśli ktoś chciałby to zrobić musiałby złamać algorytmy, na których opiera się znaczna część komunikacji w Internecie.

#include <libdinemic/dmodel.h>;
 
class MyClass : public DModel
{
    void update_network();
public:
    /// Create new object
    MyClass(StoreInterface *store,
         SyncInterface *sync,
         const std::vector<std::string> &authorized_object_ids);
    /// Restore existing object
    MyClass(const std::string &db_id, StoreInterface *store, SyncInterface *sync, DModel *parent=NULL, DModel *caller=NULL);
    /// Create as child object
    MyClass(DModel *parent);
 
    ~MyClass();
 
    DField important_property_a;
    DField important_property_b;
    DList list_of_something;
    ...
};

W dowolnym miejscu w klastrze, a dokładniej w dowolnej jego aplikacji korzystającej z libdinemic można odtworzyć obiekt, który znajduje się w bazie danych i korzystać z niego

Ok, ale co po tym, że obiekt można zmienić tylko na hoście, gdzie został stworzony? Dinemic służy do budowania aplikacji, które będą działały na wielu hostach. Koniecznym zatem jest, aby inne hosty w sieci też mogły mieć możliwość odtworzenia takich samych obiektów oraz ich modyfikacji. Pierwsza kwestia jest dość naturalna – tworząc obiekt klasy w jednej aplikacji, nie pojawi się on automatycznie w żadnym innym miejscu. Dopiero chcąc wykorzystać jego dane lub dokonać zmian w tym obiekcie musimy sami, w kodzie aplikacji stworzyć taki obiekt (czyli de facto zmienną tego typu). Dinemic zatroszczy się o to, aby jego stan był zgodny ze stanem na innych nodach klastra:

MyClass my_object("here_comes_long_id", store, sync);
my_object.important_property_a = "abcd";

W dowolnym miejscu klastra możemy stworzyć zmienną, która zostanie odwzorowana przez ORM Dinemic w obiekt o podanym ID

Co do drugiej kwestii, czyli modyfikacji obiektów (również powyższy przykład), to Dinemic daje pełną elastyczność co do definiowania co kto może i jak to może. Po pierwsze przypomnijmy – wszystkie informacje na temat zmian obiektów są zawsze dokonywane przez kogoś. Jeśli nie definiujemy tego, to są one anonimowe, czyli bez podpisu. W przeciwnym wypadku będą one podpisane kluczem prywatnym obiektu autoryzowanego do dokonania zmian lub podpisane kluczem prywatnym innego, nieautoryzowanego obiektu. Listę takich obiektów można zdefiniować już w trakcie tworzenia pierwszej instancji obiektu w klastrze, przez parametr authorized_object_ids. Wszystkie zmiany dokonywane przez wymienione tam obiekty będą traktowane tak samo jakby były dokonywane przez ten sam obiekt.

Weźmy na przykład aplikację, która ma być rozproszonym kalendarzem. Każda osoba (obiekt klasy Person) ma listę swoich wydarzeń (obiekty klasy Event). Tworząc nowe wydarzenie możemy podać ID innych osób, które mają mieć możliwość zmian np. terminu. W ten sposób przypiszemy do naszego kalendarza nasze nowe wydarzenie. Nikt poza naszymi dwoma znajomymi nie będzie mógł zmienić tej daty:

class Person : DModel {
    ...
    DField first_name;
};
 
class Event : DModel {
    ...
    DField person_id;
    DField event_date;
    DField event_name;
};
 
// Map object related to my calendar to new object in code
Person me("my_id...", store, sync);
 
// Map John to another object
Person john("jobhn's_id...", store, sync)
...
 
// Create new event in database
vector<string> authorized_objects;
authorized_objects.push_back(me.get_db_id());
authorized_objects.push_back(john.get_db_id());
...
Event new_event(store, sync, authorized_objects);

W ten sposób John będzie mógł stworzyć taki sam obiekt wydarzenia i wszelkie jego zmiany w tym obiekcie będą traktowane na równi z naszymi:

Event received_event("event_id", store, sync);
received_event.event_date = "Tomorrow!";

Powyższa zmiana, wykonana już w aplikacji John’a jest automatycznie propagowana w całym klastrze i po chwili będzie również widoczna w naszym kalendarzu.

Nieautoryzowane i anonimowe zmiany

To nie wszystko. Dinemic pozwala zaaplikować dowolny zestaw listenerów, które będą automatycznie odfiltrowywać niepodpisane lub nieautoryzowane zmiany. Dzięki listenerom możemy również nasłuchiwać zmian, jakie są wykonywane w klastrze przez innych uczestników.

Poufne dane

Kolejną rzeczą, którą dostaje się gratis jest możliwość szyfrowania danych. Ponieważ dinemic bazuje na otwartej architekturze i domyślnie pozwala akceptować wszystkie zmiany z sieci, programista tworzący w nim aplikacje ma możliwość wykorzystania istniejących kluczy kryptograficznych i zaszyfrowania poszczególnych pól w klasach. Dzięki temu tylko autoryzowane do odczytu obiekty będą miały możliwość odszyfrowania danych zapisanych w bazie i mapowanych przez framework na obiekty C++. Z punktu widzenia programisty wystarczy jedynie zmodyfikować klasę DField lub DList, ustawiając w jej konstruktorze parametr encrypted=true. Wszystkie pola z tą flagą są automatycznie szyfrowane przed wysłaniem do sieci za pomocą kluczy zapisanych w liście read_authorized_keys.

Jakie są największe różnice w stosunku do centraljen bazy danych?

Przede wszystkim Dinemic został stworzony aby zapewnić możliwie wysoką konsystencję danych i stanu klastra w przypadku awarii. Aplikacje w architekturach, które za pomocą logiki dokonują zmian w centralnej bazie danych nie są odporne na awarie sieci. Po dłuższym odcięciu od bazy realny stan klastra może się mocno różnić z zapisanymi w bazie danymi.

Dinemic postawił ten problem na pierwszym miejscu, przez co dowolne rozcięcie klastra, przez tzw. split brain nie powoduje rozjechania się stanu klastra i bazy danych. Po split brain’ie tworzą się de facto dwa oddzielne klastry Dinemic, które zaczynają żyć własnym życiem. Każdy z nich bazuje na ostatniej wersji bazy sprzed split-brain’a i modyfikuje tą bazę według własnego uznania. Po rejoin’ie, czyli po ponownym połączeniu dwóch części, wszystkie nody zaczynają się wymieniać brakującymi im aktualizacjami stanu.

W tym punkcje architektura Dinemic zaczyna się znacznie różnić od zwykłej, z centralją bazą danych lub od Blockchain’a. Po rejoin’ie może nie być możliwe doprowadzenie do konsystencji w takim klastrze za równo na warstwie bazy danych (co zostało przeniesione na barki programisty piszącego aplikacje korzystającą z tego frameworka) jak i na płaszczyźnie konfiguracji i stanu samego klastra. Zamiast tego otrzymujemy węzły klastra, które w miarę możliwości starają się doprowadzić do jednolitego stanu, jednak bez takiej gwarancji.

Dlaczego tak się dzieje? Popatrzmy na przykład na powyższą aplikację kalendarza. Co jeśli dwie osoby będące w danej chwili offline wprowadziły w swoich kalendarzach dwie sprzeczne zmiany dotyczące tego samego eventu? John przestawił godzinę na 11 rano, a my ustawiliśmy na 8 rano? Trudno podjąć decyzję, która wersja ma być prawidłowa i ostateczna. Co więcej, odrzucenie jednej, tak jak np. w blockchain’ie Bitcoin’a może doprowadzić do tego, że wydarzenie przesunięte przez Johna na 11 będzie kolidowało nam, ponieważ utworzyliśmy już o tej godzinie inne. Decyzja co w takim przypadku zrobić pozostaje już tylko nam (i John’owi). Z najbardziej oczywistych rozwiązań pozostaje nam odrzucenie obu zmian i zaproponowanie nowego czasu, np. przez wyświetlenie odpowiedniego komunikatu. Nie ma tu jednak sposobu na realne zsynchronizowanie planów dnia tych dwóch osób.

W przypadku tej samej sytuacji w aplikacji z centralną bazą danych, prawdopodobnie jedna z aktualizacji nie doszłaby do skutku, na przykład przez brak łączności lub zostałaby nadpisana przez drugą zmianę.

Z tego powodu framework Dinemic jedynie dąży do zsynchronizowania stanu obiektów w bazach poszczególnych węzłów klastra, jednak nie wymusza tego sztucznie i nie gwarantuje konsystencji. W zamian za to dostarcza pełen wgląd w historię zmian jakie odbywały się od początku istnienia takich obiektów i daje możliwość podejmowania dziań w zależności od kolejności zdarzeń.

Gdzie szukać dalej?

Dokładna instrukcja instalacji oraz dokumentacja frameworka jest dostępna pod adresem: https://dinemic.io

Leave a Reply