Перейти к содержанию

База данных

База данных COOPENOMICS — это встроенная распределённая система хранения данных платформы, организованная в таблицы для управления состояниями смарт-контрактов. Изменение данных осуществляется через специальные методы, называемые действиями, которые задаются в коде смарт-контрактов.

В процессе синхронизации каждая нода воспроизводит все действия из всех транзакций и последовательно восстанавливает актуальное состояние базы данных. База данных платформы хранится в оперативной памяти каждой PEER-ноды и используется ими для проверки новых транзакций, а также, для чтения состояния внешними приложениями.

Информацию из базы данных можно извлекать посредством API, пользуясь GET запросами с параметрами для поиска информации на контрактах. А поскольку эта база данных формируется и поддерживается всеми PEER-нодами одновременно, то она - распределённая, и одинаковая для всех.

Сравним с обычной базой данных

POSTGRES - это реляционная база данных, которая позволяет выполнять операции создания, удаления, редактирования и чтения любой записи в любой таблице. База данных, при этом, никаким образом не ограничивает разработчиков в том, как с ней работать, и не требует соблюдения каких-либо бизнес-правил при изменении данных в ней. Реализация бизнес-правил выносится разработчиками за пределы самой базы данных в отдельные приложения, которые решают: когда и кому можно прочитать, добавить, изменить или обновить данные. POSTGRES к этим правилам никакого отношения не имеет и никаких ограничений не накладывает. Бизнес-правила и информация в базе данных никак не связаны.

COOPENOMICS - это распределенная база данных, которая хранит информацию и бизнес-правила по её изменению в смарт-контрактах. Информация описываются структурами данных, а правила - программным кодом. Правила, которые описаны в программном коде смарт-контрактов позволяют изменить состояние распределенной базы данных только в том случае, если все программные условия смарт-контракта выполнены. И если обычная база никаким образом не регламентирует разработчикам когда они могут добавить информацию, а когда удалить, то смарт-контракты в распределенной базе данных - это делают. Мы не можем создать запись, если в смарт-контракте указано, что для этого пользователь должен быть пайщиком, или удалить её, если пайщик - активен. Таким образом, в COOPENOMICS база данных и правила по изменению информации в ней неразрывно связаны.

Действия и таблицы в COOPENOMICS неразрывно связаны, образуя единое целое: каждое изменение в таблице возможно только через выполнение действия, определённого в коде смарт-контракта. Нельзя напрямую изменить данные таблицы, поскольку все модификации выполняются через строго заданные правила, описанные в действиях. Это гарантирует, что любые изменения данных проходят проверку логики контракта и согласуются с правилами платформы.

Типы данных

Все структуры данных в COOPENOMICS описываются с помощью следующих типов:

Тип данных Описание
uint64_t 64-битное беззнаковое целое число. Используется для хранения идентификаторов и других числовых значений.
uint32_t 32-битное беззнаковое целое число. Применяется для меньших чисел, например, счетчиков или меток времени.
uint16_t 16-битное беззнаковое целое число. Используется для хранения небольших числовых данных.
uint8_t 8-битное беззнаковое целое число. Часто используется для хранения флагов или маленьких чисел.
int64_t 64-битное знаковое целое число. Применяется для значений, которые могут быть отрицательными.
int32_t 32-битное знаковое целое число. Используется для хранения небольших отрицательных или положительных чисел.
int16_t 16-битное знаковое целое число. Для небольших числовых данных.
int8_t 8-битное знаковое целое число. Часто используется для флагов или небольших чисел.
float64_t 64-битное число с плавающей точкой. Используется для хранения вещественных чисел с высокой точностью.
float32_t 32-битное число с плавающей точкой. Используется для вещественных чисел меньшей точности.
bool Логический тип. Хранит значение true или false.
name Специальный тип, представляющий имя аккаунта или идентификатор. Ограничен 12 символами a-z и цифрами 1-5.
asset Тип для хранения токенов с их количеством и символом валюты.
checksum256 256-битный хеш. Применяется для хранения контрольных сумм или идентификаторов.
time_point Тип для хранения меток времени с микросекундной точностью.
time_point_sec Тип для хранения меток времени с секундной точностью.
block_timestamp_type Тип для хранения временных меток блоков в сети.
std::string Тип строки для хранения текстовых данных.
std::vector<T> Массив данных заданного типа T. Используется для хранения списков или массивов данных.
std::optional<T> Тип, указывающий на возможность наличия или отсутствия значения типа T.

Структуры данных

Типы данных составляют структуры:

struct User {
    uint64_t id;          // Уникальный идентификатор пользователя
    std::string name;     // Имя пользователя
    uint32_t age;         // Возраст пользователя
};

Структуры могут содержать структуры:

struct Address {
    std::string city;     // Город
    std::string street;   // Улица
    uint32_t house_number;// Номер дома
};

struct UserWithAddress {
    uint64_t id;          // Уникальный идентификатор
    std::string name;     // Имя
    Address address;      // Адрес пользователя
};

И массивы структур:

struct Post {
    uint64_t id;          // Уникальный идентификатор поста
    std::string title;    // Заголовок поста
    std::string content;  // Содержимое поста
};

struct UserWithPosts {
    uint64_t id;                     // Уникальный идентификатор пользователя
    std::string name;                // Имя пользователя
    std::vector<Post> posts;         // Список постов пользователя
};

Также, поля в структурах могут быть не обязательными:

struct Profile {
    uint64_t id;                      // Уникальный идентификатор
    std::string username;             // Имя пользователя
    std::optional<std::string> bio;   // Биография (может отсутствовать)
};

Таблицы

Таблицы определяются указанием в структуре макроса, ключа и типа индекса.

struct [[eosio::table]] Product { # (1)
    uint64_t id;                    # (2) 
    std::string name;               # (3) 
    uint64_t category_id;           # (4)
    double price;                   # (5)

    uint64_t primary_key() const { return id; } # (6)
};

typedef eosio::multi_index<"products"_n, Product> product_table; # (7)
  1. Здесь [[eosio::table]] - это макрос, указывающий что структура - это таблица, и она должна быть отображена в API смарт-контракта для доступа извне. Этот макрос используется компилятором при генерации бинарного интерфейса смарт-контракта, который затем будет установлен на платформе.

  2. Уникальный идентификатор продукта

  3. Название продукта

  4. ID категории

  5. Цена

  6. Это основной индекс, который будет использоваться для проверки уникальности и поиска. Его тип - uint64_t, как и тип поля id, которое указано в качестве основного индекса.

  7. typedef eosio::multi_index<"products"_n, Product> product_table; - это определение типа индекса в таблице, которое заявляет что таблица будет храниться в области памяти под именем "products"_n, там будет храниться объект типа Product, и имя этому индексу - product_table.

Макрос кодировки имени аккаунта

_n - это макрос, который говорит о том, что указанную строку необходимо перевести в кодировку имен аккаунтов; на платформе имена аккаунтов кодируются в числовые аналоги, т.е. если мы видим имя аккаунта в виде строки, то блокчейн "видит" большое число, которым закодирована строка. А т.к. в именах аккаунтов допустимы только буквы a-z и цифры 1-5, а также, длинна должна быть не более 12 символов, то ими мы и ограничены в создании пространства имен таблиц, по которым они будут доступны по API. Макрос _n - это быстрый перевод указанной строки в имя аккаунта, у которого есть функциональный аналог: name("products").

Чтобы обратиться к описанной таблице в смарт-контракте необходимо:

product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)
print(product -> name); # (3)
  1. Строим индекс, к которому мы будем обращаться. Где первый контракт в скобках указывает на то имя контракта, который является хранилищем данных. Каждый контракт может запрашивать информацию из любых других контрактов, если он осведомлён о её структуре. Второй же контракт указывает на область памяти, в которой хранится информация. В данном случае мы ищем информацию в области памяти самого контракта, однако, могли бы заменить её, например, на область памяти username.value, то говорило бы о том, что записи необходимо искать в области памяти пользователя.

  2. Ищем продукт под индексом 1.

  3. Распечатываем имя продукта в консоль.

Создание записи

Описанный выше пример показывает, как на платформе реализуется поиск информации. Но как добавлять, редактировать и удалять информацию? Для этого применяются методы emplace, modify и erase на индексах.

product_table table1("contractname"_n, "contractname".value); # (1)

table1.emplace("contractname"_n, [&](auto &row){ # (2)
  row.id = 1;                                    # (3)
  row.name = "Ball";                             # (4)
  row.category_id = 1;                           # (5)
  row.price = 100;                               # (6)
})
  1. Вновь строим индекс в котором мы будем создавать запись в контракте "contractname" в глобальной области памяти самого контракта "contractname".

  2. Вызываем метод emplace, где первый параметр - это имя плательщика за системный ресурс оперативную памяти (RAM), который будет расчитан исходя из того, какое количество информации будет фактически сохранено. Вторым параметром мы передаем лямбда-функцию с названием строки, которую будем менять. Обычно эта лямбда-функция всегда одинакова для всех вызовов и её просто нужно запомнить.

  3. Устанавливаем идентификатор строки

  4. Задаём значение поля name

  5. Задаём значение категории

  6. Задаём значение идентификатора категории

Редактирование записи

Для редактирования записи, нам нужно её найти и применить метод modify:

product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)

table1.modify(product, "contractname"_n, [&](auto &row){ # (3)
  row.price = 200;  # (4)
});
  1. Формируем индекс
  2. Ищем запись по primary_key
  3. Вызываем метод modify, где первый аргумент - это указатель на ранее найденный продукт, второй - это имя аккаунта плательщика за RAM, и третий аргумент, как и ранее - это лямбда функция, которую мы применяем для того, чтобы получить доступ к строке row.
  4. Устанавливаем новую цену продукта

Удаление записи

Для удаления записи, аналогично, нам нужно её найти и применить метод erase:

product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)

table1.erase(product); # (3)
  1. Формируем индекс
  2. Ищем запись по primary_key
  3. Вызываем метод erase и передаем указатель на ранее найденный продукт

Удаление записи производится из состояния распределенной базы данных, которое хранится в оперативной памяти всех PEER-нод одновременно. Поэтому, удаленные данные могут быть найдены только в HISTORY-нодах, которые содержат подробную информацию о всех действиях и всех изменениях в таблицах, которые они повлекли.

Действия

Программный код по изменению информации в таблицах распределенной базы данных всегда находится в действиях смарт-контрактах. Невозможно изменить информацию в таблицах минуя действия смарт-контракта. Действия же могут содержать дополнительные правила и проверки, которые отражают в себе реальные бизнес-правила, по которым функционирует система. Таким образом, бизнес-правила неразрывно связаны с информацией в базе данных.

Например, прежде чем добавить запись в таблицу заявок на поставку продукта, мы проверим - а является ли пользователь - пайщиком кооператива? Но до этого - нам необходимо объявить само действие и проверить права доступа.

[[eosio::action]] createorder(eosio::name username, uint64_t product_id) { # (1)
  require_auth(username); # (2)

  product_table table1("contractname"_n, "contractname".value); # (3)
  auto product = table1.find(1); # (4)
  eosio::check(product != table1.end(), "Продукт не найден"); # (5)

  orders_table orders("contractname"_n, "contractname".value) # (6)

  orders.emplace("contractname"_n, [&](auto &row){ # (7)
    row.order_id = available_primary_key();       # (8)
    row.username = username;                       # (9)
    row.product_id = product_id;                  # (10)
  });
}
  1. Объявляем действие смарт-контракта как функцию с указанием макроса [[eosio::action]] и аргументами, которое действие будет принимать.

  2. Указываем требуемую аудентификацию, которой контракт будет требовать при вызове действия. В данном случае контракт сможет исполнить действие только в том случае, если он будет вызвано пользователем с именем аккаунта username.

  3. Формируем индекс продуктов

  4. Ищем продукт

  5. Проверяем существует ли продукт. В случае, если указатель product соответствует концу таблицу table1.end(), выполнение действия немедленно завершится с ошибкой, что продукт не найден.

  6. Формируем индекс заявок

  7. Как и ранее, применяем функцию emplace на индексе и извлекаем строку для добавления

  8. Получаем доступный primary_key, который будет расчитан автоматически

  9. Присваиваем имя пользователя

  10. Присваиваем заказу идентификатор продукта

Вывод

Распределенная база включает в себя бизнес-правила, которые нельзя обойти: невозможно добавить запись в базу данных, не пройдя хотя бы одну из проверок в действии смарт-контракта. И таких проверок может быть много, и все они могут быть достаточно сложными.

Если в действиях смарт-контракта заложен программный код, который добавляет, редактирует или удаляет запись в базе данных, то это будет произведено на всех нодах автоматически после включения транзакции в цепочку блоков истории. Обмен информацией между нодами происходит дважды в секунду.

Смарт-контракты позволяют описывать структуры таблиц и типы данных в них, а затем, создавать, редактировать, искать и удалять информацию в базе данных посредством вызова действий. Логика работы с данными в таблицах смарт-контрактов лежит также в смарт-контрактах. А вся история вызовов - сохраняется в неразрывную цепочку истории.

Не каждое действие смарт-контракта может влиять на состояние таблиц распределенной базы данных - некоторые действия могут не вносить никаких изменений в таблицы, и при этом, исполняться. Это допустимо.

Все действия из транзакций применяются к состоянию базы данных последовательно. Если одно из действий в транзакции не может пройти проверку на математических условиях программного кода смарт-контракта, то такое действие отклоняется нодой автоматически вместе со всей транзакцией и всеми действиями в ней.

Программный код смарт-контрактов вызывается в момент применения действия из транзакции. Это происходит каждый раз, когда любая PEER-нода получает входящую транзакцию от пользователя или от других нод в процессе синхронизации.

Каждое принятое действие изменяет историю блокчейна, и может влиять на состояние распределенной базы данных, создавая, редактируя или удаляя записи в таблицах смарт-контрактов. Невозможно изменить таблицы смарт-контрактов в обход программного кода их действий - они неразрывно связаны в единое целое, и этим база данных COOPENOMICS отличается от любой другой базы данных.