Skip to content

persistent state

Alexey Khudyakov edited this page Oct 23, 2018 · 2 revisions

Мы хотим хранить состояние блокчейн-приложения в базе данных для того что 1) не держать в памяти потенциально О(H) состояние, 2) не проигрывать состояние с нуля при старте. Представляется разумные использовать ту же базу, где мы храним блоки.

Требования

Что нужно обязательно

  1. Обновлять состояние приложения при коммите.

  2. Уметь проигрывать блок/транзакцию, не изменяя базы. Это необходимо для проверки отдельных транзакций и блоков. Т.к. они ещё не приняты, изменять состояние приложения они не должны

Что было бы весьма желательно

  1. Иметь возможность проверить, что если проиграть блоки заново, то мы полчим тот же результат.

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

  3. У тендерминта в блок добавляется хеш состояния приложения. Было бы неплохо в

Миграции

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

Предлагаемое API

Основные идеи предлагаемого API:

  • Пользовательское состояние хранится в той же базе данных, что и блоки
  • Пользовательское состояние обновляется во время коммита внутри одной транзакции. Таком образом изменение состояния и запись блока происходят атомарно.
  • Пользовательское состоние описывается в виде набора примитивов, подобных Map и использует интерфейс подобный MonadState
  • Для проверки тразакций/блоков монадическое действие проигрывается те изменяя базы данных

Реализация этого потребует достаточно больших изменений в коде, которые описаны дальше

Основные типы данных

Для запросов к базе вводится тип данных:

newtype Query (rw :: Access) a = Query
  { unQuery :: MaybeT (ReaderT SQL.Connection IO) a }

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

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

Примитив для пользовательских данных пока существует только один: PMap k v, прямой аналог Map с единственным отличием, что каждый ключ может быть использован лишь однажды. Подобное ограничение полезно для некоторых применений в блокчейне и несколько упрощает реализацию, но кода всё равно требуется неприятно много.

Предполагается, что пользовательское состояние описывается в виде типа подобного

data CoinState f = CoinState
  { unspent :: f (PMap (Hash Alg, Int) (PublicKey Alg, Integer))
  }

Параметр f используется для аннотирования полей и нужен для исполнения модификаций состония.

Для того, чтобы описывать изменения состояние в ответ на транзакции/блоки вводятся два тайпкласса:

class (Monad q) => ExecutorRO q where
  type Dct q :: (* -> *) -> *
  lookupKey  :: (Ord k)
             => (forall f. Lens' (Dct q f) (f (PMap k v))) -> k -> q (Maybe v)

class (ExecutorRO q) => ExecutorRW q where
  storeKey :: (Ord k, Eq v)
           => (forall f. Lens' (Dct q f) (f (PMap k v))) -> k -> v -> q ()
  dropKey  :: (Ord k, Eq v)
           => (forall f. Lens' (Dct q f) (f (PMap k v))) -> k -> q ()

Таким образом, изменение состояния будет описываться функциями вида:

updateForBlock :: forall q. ExecutorRW q => a  -> q ()
updateForTx    :: forall q. ExecutorRW q => tx -> q ()

Для этих тайпклассов существует две реализации. Одна непосредственно модифицирующая базу, и использующаяся при коммите блоков, и вторая, использующаяся при проверке блоков.

-- | Update user's state for single block. If we trying to execute
--   already commited changes transaction will only succeed if it will
--   produce exactly same results as already commited.
runBlockUpdate
  :: (FloatOut dct)
  => Height                -- ^ Height of commited block
  -> dct Persistent        -- ^ Description of user state
  -> EffectfulQ 'RW dct a  -- ^ Action to execute
  -> Query 'RW a

-- | Run transaction without changing database. Since it's used for
--   checking transactions or blocks it always uses state
--   corresponding to latest commited block.
runEphemeralQ
  :: (FloatOut dct)
  => dct Persistent    -- ^ Description of user state
  -> EphemeralQ dct a  -- ^ Action to execute
  -> Query 'RO a

Roadmap

  1. Реализациа API, описанного выше. (Практически готово)
  2. Миграция BlockStore на Query, введение MonadDB.
  3. Перевод примеров тундерминта на новое API.
  4. Миграцию ксеночена можно проводить по частям, оставляя состояние блокчейна частично в памяти, а частично держа его в базе. Это же может позволить начать отрабатывать вопрос миграций

Clone this wiki locally