Embora a execução do Wasm seja uma característica principal do Substrate, pode haver casos em que você deseje desativá-lo. Neste artigo, mostrarei como construir uma cadeia Substrate exclusivamente nativa sem Wasm.
Aviso: Pense duas vezes sobre seu projeto!
Este não é um método recomendado para construir uma cadeia Substrate.
A equipe da Parity está removendo a execução nativa: The road to the native runtime free world #62. Ou seja, estamos indo na direção oposta.
Se você deseja usar algumas bibliotecas nativas que não oferecem suporte ao no-std
dentro do runtime pallet, você deve considerar usar:
trabalhadores offchain
interface de tempo de execução (runtime_interface)
Ambos estão no nó externo, não no tempo de execução, então podem usar bibliotecas std
e não estão limitados pelo Wasm.
Configuração do Ambiente
Ambiente: substrate-node-template | tag: polkadot-v0.9.40 | commit: 700c3a1
Suponha que você queira importar uma biblioteca Rust padrão chamada rust-bert em seu runtime pallet. O rust-bert
é uma biblioteca de aprendizado de máquina que inclui modelos de linguagem grandes (LLMs - large language models no original) como o GPT2.
Primeiro, faça o download do substrate-node-template
.
git clone https://github.com/substrate-developer-hub/substrate-node-template
cd substrate-node-template
git checkout polkadot-v0.9.40
Compilando
Adicione rust-bert
como uma dependência em pallets/template/Cargo.toml
.
Você também precisa especificar getrandom
como uma dependência. Caso contrário, ele lançará um erro error: the wasm32-unknown-unknown target is not supported by default, you may need to enable the "js" feature
(Erro: o alvo wasm32-unknown-unknown não é suportado por padrão; talvez seja necessário habilitar o recurso "js" - em tradução livre)
. Para obter mais informações, consulte: https://docs.rs/getrandom/#webassembly-support
No runtime pallets, todas as suas dependências devem:
Oferecer suporte a
no-std
.O
std
não deve estar habilitado por padrão. (isso é o quedefault-features = false
realiza)
Caso contrário, você receberá um erro error[E0152]: found duplicate lang item panic_impl
(erro[E0152]: encontrado item de linguagem duplicado panic_impl - em tradução livre) ao compilar. O motivo é que o std
está vazando para o código do tempo de execução.
Você pode verificar esta pergunta feita no Stack Overflow para obter mais detalhes.
Em pallets/template/Cargo.toml
:
[dependencies]
rust-bert = { version = "0.21.0", default-features = false, features = ["remote", "download-libtorch"] }
getrandom = { version = "0.2", default-features = false, features = ["js"] }
No entanto, o rust-bert
não oferece suporte a no-std
. Mesmo se você adicionar default-feature = false
no Cargo.toml
, ainda lançará um erro error[E0152]: found duplicate lang item panic_impl
ao executar o cargo build
.
Para corrigir esse erro, você deve pular a compilação do código wasm adicionando o env SKIP_WASM_BUILD=1
.
SKIP_WASM_BUILD=1 cargo build
Rodando
Neste ponto, você deve ter construído com sucesso um Substrate exclusivamente nativo, sem wasm.
No entanto, executar o binário de destino não é simples.
--execution native
especifica a estratégia de execução como nativa em primeiro lugar.
./target/debug/node-template --dev --execution native
Error: Input("Development wasm not available")
Procure por "Development wasm not available" no código-fonte, você verá que ele é gerado no node/src/chain_spec.rs
.
pub fn development_config() -> Result<ChainSpec, String> {
let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?;
// ...
}
Como não temos wasm, devemos remover esta verificação.
let wasm_binary = WASM_BINARY.unwrap_or(&[] as &[u8]);
Recompile e execute novamente. Um novo erro ocorre:
./target/debug/node-template --dev --execution native
2023-08-31 18:10:07 Substrate Node
2023-08-31 18:10:07 ✌️ version 4.0.0-dev-700c3a186e5
2023-08-31 18:10:07 ❤️ by Substrate DevHub <https://github.com/substrate-developer-hub>, 2017-2023
2023-08-31 18:10:07 📋 Chain specification: Development
2023-08-31 18:10:07 🏷 Node name: evanescent-agreement-4299
2023-08-31 18:10:07 👤 Role: AUTHORITY
2023-08-31 18:10:07 💾 Database: RocksDb at /tmp/substrate8yJbyt/chains/dev/db/full
2023-08-31 18:10:07 ⛓ Native runtime: node-template-100 (node-template-1.tx1.au1)
Error: Service(Client(VersionInvalid("cannot deserialize module: HeapOther(\"I/O Error: UnexpectedEof\")")))
2023-08-31 18:10:08 Cannot create a runtime error=Other("cannot deserialize module: HeapOther(\"I/O Error: UnexpectedEof\")")
Esse erro é muito mais difícil de depurar. Passei muito tempo procurando a causa raiz.
Processo de Depuração
Uma busca online encontrou apenas um problema semelhante: https://github.com/paritytech/substrate/issues/7675. O desenvolvedor principal @bkchr sugeriu criar um binário wasm fictício para contornar a verificação.
Não consegui encontrar informações relevantes sobre como construir um binário wasm fictício. Portanto, decidi depurar o código passo a passo. Finalmente, encontrei a causa raiz em uma dependência do sistema chamada native_executor.rs.
Existem duas implementações de executores: WasmExecutor
e NativeElseWasmExecutor
. Quando o --execution native
é especificado, o NativeElseWasmExecutor
será usado.
O NativeElseWasmExecutor
envolve o WasmExecutor
como seu campo.
/// Implementação genérica de CodeExecutor que usa um delegate para determinar a equivalência de código wasm
/// e despachar para código nativo quando possível, recorrendo ao WasmExecutor quando não for possível.
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
/// Informações da versão nativa do tempo de execução.
native_version: NativeVersion,
/// Executor wasm de fallback.
wasm:
WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>,
}
Durante a execução do nó do Substrate, mesmo se o `NativeElseWasmExecutor` for usado, ele ainda tentará carregar o binário wasm em 2 métodos: `runtime_version` e `call`.
Vamos examinar o `runtime_version` primeiro:
impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D> {
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
Ok(self.native_version.runtime_version.clone()) // <--- Edição: Devemos retornar a versão nativa
self.wasm.runtime_version(ext, runtime_code) // <--- Original: Ele tentará carregar o binário wasm
}
}
Em seguida, vamos olhar para a função call
.
Ela primeiro verificará se a versão nativa é compatível com a versão em cadeia. Se for compatível, ela chamará o executor nativo. Caso contrário, chamará o executor wasm.
No entanto, não temos um binário wasm válido, então sempre devemos chamar o executor nativo. Eu apenas uso esse código se if use_native && can_call_with {}
.
impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecutor<D> {
type Error = Error;
fn call(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
method: &str,
data: &[u8],
use_native: bool,
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
// Edição
// Não verifica wasm, pois é fictício, usa execução nativa diretamente
let used_native = true;
let mut ext = AssertUnwindSafe(ext);
let result = match with_externalities_safe(&mut **ext, move || D::dispatch(method, data)) {
Ok(Some(value)) => Ok(value),
Ok(None) => Err(Error::MethodNotFound(method.to_owned())),
Err(err) => Err(err),
};
(result, used_native)
// Original
tracing::trace!(
target: "executor",
function = %method,
"Executing function",
);
let on_chain_heap_alloc_strategy = runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy);
let heap_alloc_strategy = match context {
CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let mut used_native = false;
let result = self.wasm.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, onchain_version, mut ext| {
let onchain_version =
onchain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
let can_call_with =
onchain_version.can_call_with(&self.native_version.runtime_version);
if use_native && can_call_with {
// chama o executor nativo
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %onchain_version,
"Request for native execution succeeded",
);
used_native = true;
Ok(with_externalities_safe(&mut **ext, move || D::dispatch(method, data))?
.ok_or_else(|| Error::MethodNotFound(method.to_owned())))
} else {
// chama o executor wasm
if !can_call_with {
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %onchain_version,
"Request for native execution failed",
);
}
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
}
},
);
(result, used_native)
}
}
Concluído!
Você pode usar meu fork do repositório do Substrate, onde o problema já está corrigido.
Você deve substituir o URL das dependências em 3 arquivos Cargo.toml
:
git = "https://github.com/paritytech/substrate.git" -> git = "https://github.com/doutv/substrate.git"
Compile e execute de novo, e deve funcionar!
SKIP_WASM_BUILD=1 cargo build
./target/debug/node-template --dev --execution native
Conseguimos construir com sucesso uma cadeia Substrate exclusivamente nativa, sem wasm!
Código final: fork do repositório do substrate-node-template na branch native-only-polkadot-v0.9.40, https://github.com/doutv/substrate-node-template/tree/native-only-polkadot-v0.9.40
Melhorias Futuras
Modificar o NativeElseWasmExecutor
não é a melhor solução, você pode adicionar uma nova implementação executora NativeOnlyExecutor
.
Este artigo foi escrito por Backdoor e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui
Latest comments (0)