use super::{
capability::{DbRead, DbVersion, DbWrite, MigrationStatus},
router::Router,
};
use crate::{
chain_index::{
finalised_state::{
capability::DbMetadata,
entry::{StoredEntryFixed, StoredEntryVar},
finalised_source::v1::SYNC_CHECKPOINT_INTERVAL,
router::EphemeralMode,
},
source::BlockchainSource,
types::GENESIS_HEIGHT,
},
config::ChainIndexConfig,
error::FinalisedStateError,
Height, TransparentTxList, TxLocation, TxidList, ZainoVersionedSerde as _,
};
use lmdb::{Transaction, WriteFlags};
use crate::SendFut;
use std::sync::Arc;
use tracing::info;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrationType {
Patch,
Minor,
Major,
}
pub trait Migration<T: BlockchainSource> {
const CURRENT_VERSION: DbVersion;
const TO_VERSION: DbVersion;
fn current_version(&self) -> DbVersion {
Self::CURRENT_VERSION
}
fn to_version(&self) -> DbVersion {
Self::TO_VERSION
}
fn migration_type(&self) -> MigrationType {
MigrationType::Patch
}
fn migrate(
&self,
router: Arc<Router<T>>,
_cfg: ChainIndexConfig,
_source: T,
) -> impl SendFut<Result<(), FinalisedStateError>> {
async move {
info!(
from = %Self::CURRENT_VERSION,
to = %Self::TO_VERSION,
"starting metadata-only migration"
);
let mut metadata: DbMetadata = router.get_metadata().await?;
metadata.version = Self::TO_VERSION;
metadata.schema_hash =
crate::chain_index::finalised_state::finalised_source::v1::DB_SCHEMA_V1_HASH;
metadata.migration_status = MigrationStatus::Empty;
router.update_metadata(metadata).await?;
info!(
from = %Self::CURRENT_VERSION,
to = %Self::TO_VERSION,
"metadata-only migration complete"
);
Ok(())
}
}
}
pub(super) struct MigrationManager<T: BlockchainSource> {
pub(super) router: Arc<Router<T>>,
pub(super) cfg: ChainIndexConfig,
pub(super) current_version: DbVersion,
pub(super) target_version: DbVersion,
pub(super) source: T,
}
impl<T: BlockchainSource> MigrationManager<T> {
pub(super) async fn migrate(&mut self) -> Result<(), FinalisedStateError> {
while self.current_version < self.target_version {
let migration = self.get_migration()?;
let migration_type = migration.migration_type::<T>();
match migration_type {
MigrationType::Patch => {
migration
.migrate(
Arc::clone(&self.router),
self.cfg.clone(),
self.source.clone(),
)
.await?;
}
MigrationType::Minor | MigrationType::Major => {
let primary = self.router.primary_backend();
let db_height = primary.db_height().await?;
let _ephemeral_reference = self
.router
.init_or_take_ephemeral(
self.source.clone(),
self.cfg.network.to_zebra_network(),
EphemeralMode::Full,
db_height,
)
.await?;
migration
.migrate(
Arc::clone(&self.router),
self.cfg.clone(),
self.source.clone(),
)
.await?;
}
}
self.current_version = migration.to_version::<T>();
}
Ok(())
}
fn get_migration(&self) -> Result<MigrationStep, FinalisedStateError> {
match (
self.current_version.major,
self.current_version.minor,
self.current_version.patch,
) {
(1, 0, 0) => Ok(MigrationStep::Migration1_0_0To1_1_0(Migration1_0_0To1_1_0)),
(1, 1, 0) => Ok(MigrationStep::Migration1_1_0To1_2_0(Migration1_1_0To1_2_0)),
(1, 2, 0) => Ok(MigrationStep::Migration1_2_0To1_2_1(Migration1_2_0To1_2_1)),
(_, _, _) => Err(FinalisedStateError::Custom(format!(
"Missing migration from version {}",
self.current_version
))),
}
}
}
enum MigrationStep {
Migration1_0_0To1_1_0(Migration1_0_0To1_1_0),
Migration1_1_0To1_2_0(Migration1_1_0To1_2_0),
Migration1_2_0To1_2_1(Migration1_2_0To1_2_1),
}
impl MigrationStep {
fn to_version<T: BlockchainSource>(&self) -> DbVersion {
match self {
MigrationStep::Migration1_0_0To1_1_0(_step) => {
<Migration1_0_0To1_1_0 as Migration<T>>::TO_VERSION
}
MigrationStep::Migration1_1_0To1_2_0(_step) => {
<Migration1_1_0To1_2_0 as Migration<T>>::TO_VERSION
}
MigrationStep::Migration1_2_0To1_2_1(_step) => {
<Migration1_2_0To1_2_1 as Migration<T>>::TO_VERSION
}
}
}
fn migration_type<T: BlockchainSource>(&self) -> MigrationType {
match self {
MigrationStep::Migration1_0_0To1_1_0(step) => {
<Migration1_0_0To1_1_0 as Migration<T>>::migration_type(step)
}
MigrationStep::Migration1_1_0To1_2_0(step) => {
<Migration1_1_0To1_2_0 as Migration<T>>::migration_type(step)
}
MigrationStep::Migration1_2_0To1_2_1(step) => {
<Migration1_2_0To1_2_1 as Migration<T>>::migration_type(step)
}
}
}
async fn migrate<T: BlockchainSource>(
&self,
router: Arc<Router<T>>,
cfg: ChainIndexConfig,
source: T,
) -> Result<(), FinalisedStateError> {
match self {
MigrationStep::Migration1_0_0To1_1_0(step) => step.migrate(router, cfg, source).await,
MigrationStep::Migration1_1_0To1_2_0(step) => step.migrate(router, cfg, source).await,
MigrationStep::Migration1_2_0To1_2_1(step) => step.migrate(router, cfg, source).await,
}
}
}
struct Migration1_0_0To1_1_0;
impl<T: BlockchainSource> Migration<T> for Migration1_0_0To1_1_0 {
const CURRENT_VERSION: DbVersion = DbVersion {
major: 1,
minor: 0,
patch: 0,
};
const TO_VERSION: DbVersion = DbVersion {
major: 1,
minor: 1,
patch: 0,
};
}
struct Migration1_1_0To1_2_0;
fn flush_migration_spent_batch(
env: &lmdb::Environment,
spent_db: lmdb::Database,
metadata_db: lmdb::Database,
progress_key: &[u8],
buffer: &mut Vec<(Vec<u8>, TxLocation)>,
up_to_height: Height,
) -> Result<(), FinalisedStateError> {
buffer.sort_by(|a, b| a.0.cmp(&b.0));
let mut txn = env.begin_rw_txn()?;
for (outpoint_bytes, tx_location) in buffer.iter() {
let entry_bytes = StoredEntryFixed::new(outpoint_bytes, *tx_location).to_bytes()?;
match txn.put(
spent_db,
outpoint_bytes,
&entry_bytes,
WriteFlags::NO_OVERWRITE,
) {
Ok(()) => {}
Err(lmdb::Error::KeyExist) => {
let existing = txn
.get(spent_db, outpoint_bytes)
.map_err(FinalisedStateError::LmdbError)?;
if existing != entry_bytes {
return Err(FinalisedStateError::Custom(format!(
"conflicting existing spent entry during batched migration for outpoint {}",
hex::encode(outpoint_bytes)
)));
}
}
Err(error) => return Err(FinalisedStateError::LmdbError(error)),
}
}
let progress = StoredEntryFixed::new(progress_key, up_to_height + 1);
txn.put(
metadata_db,
&progress_key,
&progress.to_bytes()?,
WriteFlags::empty(),
)?;
txn.commit()?;
env.sync(true)?;
buffer.clear();
Ok(())
}
impl<T: BlockchainSource> Migration<T> for Migration1_1_0To1_2_0 {
const CURRENT_VERSION: DbVersion = DbVersion {
major: 1,
minor: 1,
patch: 0,
};
const TO_VERSION: DbVersion = DbVersion {
major: 1,
minor: 2,
patch: 0,
};
fn migration_type(&self) -> MigrationType {
MigrationType::Minor
}
async fn migrate(
&self,
router: Arc<Router<T>>,
cfg: ChainIndexConfig,
_source: T,
) -> Result<(), FinalisedStateError> {
const MIGRATION_TXID_LOCATION_PROGRESS_KEY: &[u8] =
b"_migration_txid_location_progress_1_2_0_next_height";
const MIGRATION_SPENT_PROGRESS_KEY: &[u8] = b"_migration_spent_progress_1_2_0_next_height";
info!("Starting v1.1.0 → v1.2.0 migration.");
let backend = router.primary_backend();
let env = backend.env()?;
let metadata_db = backend.metadata_db()?;
let txids_db = backend.txids_db()?;
let transparent_db = backend.transparent_db()?;
let spent_db = backend.spent_db()?;
let txid_location_db = backend.txid_location_db()?;
{
let mut metadata: DbMetadata = backend.get_metadata().await?;
if metadata.migration_status == MigrationStatus::Empty {
metadata.migration_status = MigrationStatus::PartialBuidInProgress;
backend.update_metadata(metadata).await?;
}
}
let read_progress = |key: &[u8]| -> Result<Option<u32>, FinalisedStateError> {
let txn = env.begin_ro_txn()?;
match txn.get(metadata_db, &key) {
Ok(bytes) => {
let entry = StoredEntryFixed::<Height>::from_bytes(bytes).map_err(|error| {
FinalisedStateError::Custom(format!(
"corrupt v1.2.0 migration progress entry: {error}"
))
})?;
if !entry.verify(key) {
return Err(FinalisedStateError::Custom(
"v1.2.0 migration progress checksum mismatch".to_string(),
));
}
Ok(Some(entry.inner().0))
}
Err(lmdb::Error::NotFound) => Ok(None),
Err(error) => Err(FinalisedStateError::LmdbError(error)),
}
};
if let Some(db_tip) = backend.db_height().await? {
let db_tip = db_tip.0;
let mut next_height =
read_progress(MIGRATION_TXID_LOCATION_PROGRESS_KEY)?.unwrap_or(GENESIS_HEIGHT.0);
info!(
resume_height = next_height,
db_tip, "v1.2.0 migration Stage A: building txid_location index"
);
let stage_a_started = std::time::Instant::now();
while next_height <= db_tip {
let height = Height::try_from(next_height)
.map_err(|error| FinalisedStateError::Custom(error.to_string()))?;
let height_bytes = height.to_bytes()?;
let txids = {
let txn = env.begin_ro_txn()?;
let raw = txn
.get(txids_db, &height_bytes)
.map_err(FinalisedStateError::LmdbError)?;
let entry = StoredEntryVar::<TxidList>::from_bytes(raw).map_err(|error| {
FinalisedStateError::Custom(format!("txids corrupt data: {error}"))
})?;
if !entry.verify(&height_bytes) {
return Err(FinalisedStateError::Custom(
"txids checksum mismatch".to_string(),
));
}
entry.inner().txids().to_vec()
};
let mut entries: Vec<([u8; 32], TxLocation)> = Vec::with_capacity(txids.len());
for (tx_index, txid) in txids.iter().enumerate() {
let tx_index = u16::try_from(tx_index).map_err(|_| {
FinalisedStateError::Custom(format!(
"transaction index out of range at height {}",
height.0
))
})?;
entries.push(((*txid).into(), TxLocation::new(height.0, tx_index)));
}
entries.sort_by_key(|entry| entry.0);
{
let mut txn = env.begin_rw_txn()?;
for (txid_bytes, tx_location) in &entries {
let entry_bytes =
StoredEntryFixed::new(txid_bytes, *tx_location).to_bytes()?;
match txn.put(
txid_location_db,
txid_bytes,
&entry_bytes,
WriteFlags::NO_OVERWRITE,
) {
Ok(()) => {}
Err(lmdb::Error::KeyExist) => {
let existing_bytes = txn
.get(txid_location_db, txid_bytes)
.map_err(FinalisedStateError::LmdbError)?;
let existing_entry =
StoredEntryFixed::<TxLocation>::from_bytes(existing_bytes)
.map_err(|error| {
FinalisedStateError::Custom(format!(
"corrupt existing txid_location entry: {error}"
))
})?;
if !existing_entry.verify(txid_bytes) {
return Err(FinalisedStateError::Custom(
"existing txid_location entry checksum mismatch"
.to_string(),
));
}
if existing_entry.inner() != tx_location {
return Err(FinalisedStateError::Custom(format!(
"conflicting txid_location entry at height {}",
height.0
)));
}
}
Err(error) => return Err(FinalisedStateError::LmdbError(error)),
}
}
let progress =
StoredEntryFixed::new(MIGRATION_TXID_LOCATION_PROGRESS_KEY, height + 1);
txn.put(
metadata_db,
&MIGRATION_TXID_LOCATION_PROGRESS_KEY,
&progress.to_bytes()?,
WriteFlags::empty(),
)?;
txn.commit()?;
}
if next_height % SYNC_CHECKPOINT_INTERVAL == 0 {
env.sync(true)?;
}
if next_height % 50_000 == 0 {
info!(
height = next_height,
db_tip,
elapsed = ?stage_a_started.elapsed(),
"v1.2.0 migration Stage A progress"
);
}
next_height = height.0 + 1;
}
env.sync(true)?;
info!(
db_tip,
elapsed = ?stage_a_started.elapsed(),
"v1.2.0 migration Stage A complete"
);
let mut next_height_to_migrate = match read_progress(MIGRATION_SPENT_PROGRESS_KEY)? {
Some(height) => height,
None => {
let mut txn = env.begin_rw_txn()?;
let progress =
StoredEntryFixed::new(MIGRATION_SPENT_PROGRESS_KEY, GENESIS_HEIGHT);
txn.put(
metadata_db,
&MIGRATION_SPENT_PROGRESS_KEY,
&progress.to_bytes()?,
WriteFlags::empty(),
)?;
txn.commit()?;
GENESIS_HEIGHT.0
}
};
let db_tip = router
.db_height()
.await?
.map(|height| height.0)
.unwrap_or(db_tip);
info!(
resume_height = next_height_to_migrate,
db_tip, "v1.2.0 migration Stage B: backfilling spent index"
);
let stage_b_started = std::time::Instant::now();
let batch_budget =
(cfg.storage.database.sync_write_batch_size.to_byte_count() as u64).max(1);
let mut spent_buffer: Vec<(Vec<u8>, TxLocation)> = Vec::new();
let mut spent_buffer_bytes: u64 = 0;
while next_height_to_migrate <= db_tip {
let height = Height::try_from(next_height_to_migrate)
.map_err(|error| FinalisedStateError::Custom(error.to_string()))?;
let height_bytes = height.to_bytes()?;
let transparent_tx_list = {
let txn = env.begin_ro_txn()?;
let raw = txn
.get(transparent_db, &height_bytes)
.map_err(FinalisedStateError::LmdbError)?;
let entry =
StoredEntryVar::<TransparentTxList>::from_bytes(raw).map_err(|error| {
FinalisedStateError::Custom(format!(
"transparent corrupt data: {error}"
))
})?;
if !entry.verify(&height_bytes) {
return Err(FinalisedStateError::Custom(
"transparent checksum mismatch".to_string(),
));
}
entry.inner().clone()
};
let transparent = transparent_tx_list.tx().to_vec();
let mut spent_map = std::collections::HashMap::new();
for (tx_index, tx_opt) in transparent.iter().enumerate() {
let Some(transparent_tx) = tx_opt else {
continue;
};
let tx_index = u16::try_from(tx_index).map_err(|_| {
FinalisedStateError::Custom(format!(
"transaction index out of range at height {}",
height.0
))
})?;
let tx_location = TxLocation::new(height.0, tx_index);
for outpoint in transparent_tx.spent_outpoints() {
if spent_map.insert(outpoint, tx_location).is_some() {
return Err(FinalisedStateError::Custom(format!(
"duplicate transparent spend for outpoint {:?} at height {}",
outpoint, height.0
)));
}
}
}
for (outpoint, tx_location) in &spent_map {
let outpoint_bytes = outpoint.to_bytes()?;
spent_buffer_bytes =
spent_buffer_bytes.saturating_add(outpoint_bytes.len() as u64 + 64);
spent_buffer.push((outpoint_bytes, *tx_location));
}
if spent_buffer_bytes >= batch_budget {
flush_migration_spent_batch(
&env,
spent_db,
metadata_db,
MIGRATION_SPENT_PROGRESS_KEY,
&mut spent_buffer,
height,
)?;
spent_buffer_bytes = 0;
}
if next_height_to_migrate % 10_000 == 0 {
info!(
height = next_height_to_migrate,
db_tip,
elapsed = ?stage_b_started.elapsed(),
"v1.2.0 migration Stage B progress"
);
}
next_height_to_migrate = height.0 + 1;
}
if !spent_buffer.is_empty() {
let tip_height = Height::try_from(db_tip)
.map_err(|error| FinalisedStateError::Custom(error.to_string()))?;
flush_migration_spent_batch(
&env,
spent_db,
metadata_db,
MIGRATION_SPENT_PROGRESS_KEY,
&mut spent_buffer,
tip_height,
)?;
}
info!(
db_tip,
elapsed = ?stage_b_started.elapsed(),
"v1.2.0 migration Stage B complete"
);
backend.run_v1_2_migration_accumulator_stage(db_tip).await?;
}
env.sync(true)?;
let mut metadata: DbMetadata = backend.get_metadata().await?;
metadata.version = <Self as Migration<T>>::TO_VERSION;
metadata.schema_hash =
crate::chain_index::finalised_state::finalised_source::v1::DB_SCHEMA_V1_HASH;
metadata.migration_status = MigrationStatus::Empty;
backend.update_metadata(metadata).await?;
env.sync(true)?;
{
let mut txn = env.begin_rw_txn()?;
for key in [
MIGRATION_TXID_LOCATION_PROGRESS_KEY,
MIGRATION_SPENT_PROGRESS_KEY,
] {
match txn.del(metadata_db, &key, None) {
Ok(()) | Err(lmdb::Error::NotFound) => {}
Err(error) => return Err(FinalisedStateError::LmdbError(error)),
}
}
txn.commit()?;
}
env.sync(true)?;
info!("v1.1.0 to v1.2.0 migration complete.");
Ok(())
}
}
struct Migration1_2_0To1_2_1;
impl<T: BlockchainSource> Migration<T> for Migration1_2_0To1_2_1 {
const CURRENT_VERSION: DbVersion = DbVersion {
major: 1,
minor: 2,
patch: 0,
};
const TO_VERSION: DbVersion = DbVersion {
major: 1,
minor: 2,
patch: 1,
};
}