Durante atualizações de sistemas blockchain, a migração de dados costuma ser a etapa mais delicada. Se você ainda enfrenta a conversão manual de dados on-chain, a ferramenta de migração de armazenamento do Substrate (State Trie Migration) oferece uma solução completa de automação, evitando que a atualização de dados se torne um obstáculo para a evolução da blockchain. Este artigo apresenta como usar essa ferramenta para lidar com os desafios de migração de dados em atualizações de sistemas blockchain.
Desafios centrais da migração de armazenamento
Quando um sistema blockchain é atualizado, mudanças na estrutura de dados costumam trazer grandes riscos. O método tradicional de migração manual não é apenas demorado, mas também pode causar perda ou inconsistência de dados devido a erros humanos. Como plataforma para inovadores em blockchain, o Substrate projeta sua ferramenta de migração de armazenamento especificamente para resolver esses problemas.
Principais dificuldades da migração de armazenamento
- Consistência de dados: garantir que os dados não sejam perdidos nem corrompidos durante a migração
- Disponibilidade do sistema: minimizar o impacto no funcionamento normal da blockchain durante a migração
- Controle de recursos: alocar razoavelmente largura de banda, armazenamento dos nós e recursos de computação
- Compatibilidade de versões: suportar conversão de formato de dados entre diferentes versões
A ferramenta de migração de armazenamento do Substrate resolve efetivamente esses desafios por meio de processamento automatizado e controle inteligente de recursos. O código central está em frame/state-trie-migration/src/lib.rs; esse módulo fornece funcionalidade completa de migração de armazenamento.
Os dois modos da ferramenta de migração de armazenamento
A ferramenta de migração de armazenamento do Substrate oferece dois modos complementares de migração, que podem ser escolhidos conforme o cenário da blockchain.
Modo de migração automática
O modo de migração automática executa tarefas de migração automaticamente no início de cada bloco por meio da função on_initialize. Esse modo é especialmente adequado para relay chains ou chains independentes, onde ultrapassar ligeiramente os limites de peso não causa problemas graves.
// Implementação central da migração automática
fn migrate_until_exhaustion(
&mut self,
limites: MigrationLimits,
) -> Result<(), Error<T>> {
log!(debug, "executando migrações sobre {:?} até {:?}", self, limites);
if limites.item.is_zero() || limites.size.is_zero() {
// Trata o caso limite; caso contrário, chamaríamos `migrate_tick` pelo menos uma vez
log!(warn, "limites são zero; interrompendo");
return Ok(())
}
while !self.exhausted(limites) && !self.finished() {
if let Err(erro) = self.migrate_tick() {
log!(error, "migrate_until_exhaustion falhou: {:?}", erro);
return Err(erro)
}
}
// Acumula os dados dinâmicos nos itens de armazenamento
self.size = self.size.saturating_add(self.dyn_size);
self.child_items = self.child_items.saturating_add(self.dyn_child_items);
self.top_items = self.top_items.saturating_add(self.dyn_top_items);
log!(debug, "concluído com {:?}", self);
Ok(())
}
A configuração da migração automática pode ser gerenciada pela função control_auto_migration, que permite definir os limites de recursos da migração:
/// Controla a migração automática
#[pallet::call_index(0)]
#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
pub fn control_auto_migration(
origin: OriginFor<T>,
maybe_config: Option<MigrationLimits>,
) -> DispatchResult {
T::ControlOrigin::ensure_origin(origin)?;
AutoLimits::<T>::put(maybe_config);
Ok(())
}
Modo de migração assinada
Como complemento à migração automática, o modo de migração assinada permite disparar o processo de migração por meio de transações assinadas. Essa abordagem permite declarar antecipadamente os recursos que serão consumidos e pagar a taxa correspondente, sendo uma alternativa segura quando a migração automática não é viável.
A implementação central da migração assinada está na função continue_migrate:
/// Continua a migração usando os `limites` fornecidos
#[pallet::call_index(1)]
#[pallet::weight(
// Processo de migração
Pallet::<T>::dynamic_weight(limites.item, *real_size_upper)
// Outras operações, como depósitos etc.
+ T::WeightInfo::continue_migrate()
)]
pub fn continue_migrate(
origin: OriginFor<T>,
limites: MigrationLimits,
real_size_upper: u32,
witness_task: MigrationTask<T>,
) -> DispatchResultWithPostInfo {
// Detalhes de implementação...
}
A principle vantagem do modo de migração assinada é o controle preciso do consumo de recursos, mas o chamador precisa estimar previamente os recursos necessários para a migração.
Rastreamento do progresso da migração
A ferramenta de migração de armazenamento do Substrate fornece mecanismo completo de rastreamento de progresso, garantindo que o processo seja monitorável e recuperável.
Estrutura da tarefa de migração
O progresso da migração é rastreado pela estrutura MigrationTask, que registra o estado de migração das chaves de nível superior e das chaves filhas:
/// Tarefa de migração armazenada no estado
///
/// Ela rastreia a última chave de nível superior e a última chave filha lidas
#[derive(Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, MaxEncodedLen)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct MigrationTask<T: Config> {
/// Progresso atual da migração da trie de nível superior
pub(crate) progress_top: ProgressOf<T>,
/// Progresso atual da migração da sub-trie
///
/// Se for `ToStart`, nenhuma outra chave de nível superior será processada
/// antes que a migração das chaves filhas seja concluída
pub(crate) progress_child: ProgressOf<T>,
/// Contador dinâmico de itens processados na trie de nível superior nesta execução
///
/// Não é gravado no armazenamento
#[codec(skip)]
pub(crate) dyn_top_items: u32,
/// Contador dinâmico de itens processados em qualquer sub-trie nesta execução
///
/// Não é gravado no armazenamento
#[codec(skip)]
pub(crate) dyn_child_items: u32,
/// Contador dinâmico do tamanho em bytes dos itens processados nesta execução
///
/// Não é gravado no armazenamento
#[codec(skip)]
pub(crate) dyn_size: u32,
/// Tamanho total da migração, ao longo de todas as execuções
///
/// Usado apenas para registro e depuração
pub(crate) size: u32,
/// Número total de chaves de nível superior na migração, ao longo de todas as execuções
///
/// Usado apenas para registro e depuração
pub(crate) top_items: u32,
/// Número total de chaves filhas na migração, ao longo de todas as execuções
///
/// Usado apenas para registro e depuração
pub(crate) child_items: u32,
#[codec(skip)]
pub(crate) _ph: sp_std::marker::PhantomData<T>,
}
Armazenamento do progresso
O progresso da migração é persistido no estado da blockchain por meio do item de armazenamento MigrationProcess:
/// Progresso da migração
///
/// Armazena um instantâneo das últimas chaves migradas. Pode ser iniciado e avançado
/// por qualquer um dos meios fornecidos por este módulo
#[pallet::storage]
#[pallet::getter(fn migration_process)]
pub type MigrationProcess<T> = StorageValue<_, MigrationTask<T>, ValueQuery>;
Funcionalidade de migração personalizada
Além dos modos padrão, a ferramenta de migração de armazenamento do Substrate oferece funcionalidade de migração personalizada para lidar com casos especiais ou corrigir problemas que possam surgir durante a migração.
Migração personalizada de chaves de nível superior
A função migrate_custom_top permite especificar manualmente a lista de chaves de nível superior que precisam ser migradas:
/// Migra uma lista de chaves de nível superior iterando uma a uma
#[pallet::call_index(2)]
#[pallet::weight(
T::WeightInfo::migrate_custom_top_success()
.max(T::WeightInfo::migrate_custom_top_fail())
.saturating_add(
Pallet::<T>::dynamic_weight(keys.len() as u32, *witness_size)
)
)]
pub fn migrate_custom_top(
origin: OriginFor<T>,
keys: Vec<Vec<u8>>,
witness_size: u32,
) -> DispatchResultWithPostInfo {
// Detalhes de implementação...
}
Migração personalizada de chaves filhas
De forma semelhante, a função migrate_custom_child é usada para migrar chaves em uma sub-trie específica:
/// Migra uma lista de chaves filhas em uma sub-trie específica iterando uma a uma
#[pallet::call_index(3)]
#[pallet::weight(
T::WeightInfo::migrate_custom_child_success()
.max(T::WeightInfo::migrate_custom_child_fail())
.saturating_add(
Pallet::<T>::dynamic_weight(child_keys.len() as u32, *total_size)
)
)]
pub fn migrate_custom_child(
origin: OriginFor<T>,
root: Vec<u8>,
child_keys: Vec<Vec<u8>>,
total_size: u32,
) -> DispatchResultWithPostInfo {
// Detalhes de implementação...
}
Configuração e limites da migração
A ferramenta de migração de armazenamento do Substrate oferece opções de configuração flexíveis, que podem ser ajustadas de acordo com as necessidades de cada blockchain.
Parâmetros de configuração
A principal configuração da ferramenta de migração é definida pelo trait Config:
/// Configuração deste módulo
#[pallet::config]
pub trait Config: frame_system::Config {
/// Origin que pode controlar a configuração deste módulo
type ControlOrigin: frame_support::traits::EnsureOrigin<Self::RuntimeOrigin>;
/// Filtro de Origin que dispara migrações manuais
type SignedFilter: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
/// Tipo de evento de nível superior
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Tipo provedor de moeda
type Currency: Currency<Self::AccountId>;
/// Número máximo de bytes que uma chave pode ter
#[pallet::constant]
type MaxKeyLen: Get<u32>;
/// Depósito por item pré-coletado para migração assinada
type SignedDepositPerItem: Get<BalanceOf<Self>>;
/// Valor base para [`Config::SignedDepositPerItem`]
///
/// O depósito final é `items * SignedDepositPerItem + SignedDepositBase`
type SignedDepositBase: Get<BalanceOf<Self>>;
/// Informações de peso deste módulo
type WeightInfo: WeightInfo;
}
Limites de migração
O consumo de recursos durante a migração é controlado pela estrutura MigrationLimits:
/// Limites da migração
#[derive(
Clone,
Copy,
Encode,
Decode,
scale_info::TypeInfo,
Default,
Debug,
PartialEq,
Eq,
MaxEncodedLen,
)]
pub struct MigrationLimits {
/// Limite de tamanho em bytes
pub size: u32,
/// Limite de quantidade de chaves
pub item: u32,
}
Monitoramento e tratamento de erros da migração
A ferramenta de migração de armazenamento do Substrate fornece mecanismos completos de monitoramento e tratamento de erros, garantindo confiabilidade e recuperabilidade do processo.
Eventos de migração
Vários eventos são disparados durante a migração para monitorar o progresso e o estado:
/// Eventos internos deste módulo
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Migrou respectivamente a quantidade especificada de chaves `(top, child)`,
/// usando o método de `compute` fornecido
Migrated { top: u32, child: u32, compute: MigrationCompute },
/// Uma conta foi penalizada com o valor especificado
Slashed { who: T::AccountId, amount: BalanceOf<T> },
/// A tarefa de migração automática foi concluída
AutoMigrationFinished,
/// A migração foi interrompida devido a erro ou configuração incorreta
Halted { error: Error<T> },
}
Tratamento de erros
Erros que podem ocorrer durante a migração são definidos pelo enum Error:
#[pallet::error]
#[derive(Clone, PartialEq)]
pub enum Error<T> {
/// Limite máximo assinado não respeitado
MaxSignedLimits,
/// Comprimento da chave maior que o máximo configurado
KeyTooLong,
/// Remetente com fundos insuficientes
NotEnoughFunds,
/// Dados de testemunha fornecidos incorretos
BadWitness,
/// Migração assinada não permitida porque o limite máximo ainda não foi definido
SignedMigrationNotAllowed,
/// Raiz filha fornecida incorreta
BadChildRoot,
}
Quando ocorre um erro durante a migração, o sistema dispara o evento Halted e pausa o processo, aguardando intervenção manual.
Guia de aplicação prática
Para usar a ferramenta de migração de armazenamento do Substrate em um projeto real, siga estas etapas:
- Configurar parâmetros de migração: de acordo com as necessidades específicas da blockchain, defina limites de migração e parâmetros de depósito adequados
- Escolher o modo de migração: selecione o modo adequado conforme o tipo de chain (relay chain ou parachain)
- Monitorar o progresso da migração: acompanhe o processo por meio de eventos para garantir que tudo ocorra como esperado
- Preparar um plano de contingência: conheça a funcionalidade de migração personalizada para intervir manualmente quando necessário
A implementação completa da ferramenta de migração pode ser encontrada em frame/state-trie-migration/src/lib.rs, e as definições de peso estão em frame/state-trie-migration/src/weights.rs.