-
Notifications
You must be signed in to change notification settings - Fork 0
persistent state
Мы хотим хранить состояние блокчейн-приложения в базе данных для того что 1) не держать в памяти потенциально О(H) состояние, 2) не проигрывать состояние с нуля при старте. Представляется разумные использовать ту же базу, где мы храним блоки.
Что нужно обязательно
-
Обновлять состояние приложения при коммите.
-
Уметь проигрывать блок/транзакцию, не изменяя базы. Это необходимо для проверки отдельных транзакций и блоков. Т.к. они ещё не приняты, изменять состояние приложения они не должны
Что было бы весьма желательно
-
Иметь возможность проверить, что если проиграть блоки заново, то мы полчим тот же результат.
-
Вероятно, несколько менее полезно, но может быть полезно для реализации предыдущего пункта - иметь возможность увидеть состояние приложения на людой высоте. Т.е. реализовать персистентные структуры данных в базе.
-
У тендерминта в блок добавляется хеш состояния приложения. Было бы неплохо в
Добавление персистентного состояния сразу же добавляет сложности. В первую очередь, это миграции. До этого момента, все структуры данных, производные от блоков жили только в памяти, и мы могли их менять свободно. Теперь на каждое изменение нам придётся приделывать миграцию
Основные идеи предлагаемого 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- Реализациа API, описанного выше. (Практически готово)
- Миграция
BlockStoreнаQuery, введениеMonadDB. - Перевод примеров тундерминта на новое API.
- Миграцию ксеночена можно проводить по частям, оставляя состояние блокчейна частично в памяти, а частично держа его в базе. Это же может позволить начать отрабатывать вопрос миграций