Skip to content

Instantly share code, notes, and snippets.

@homura
Last active May 8, 2022 15:12
Show Gist options
  • Save homura/140470ffb4aa46495078b2aa18be3e46 to your computer and use it in GitHub Desktop.
Save homura/140470ffb4aa46495078b2aa18be3e46 to your computer and use it in GitHub Desktop.
title date tags categories
瞥一眼 Substrate 02 —— 升级
2020-10-15
blockchain
substrate
wasm
executor
瞥一眼 Substrate

Gavin Wood 在 2018 年 Web3 峰会上演示了从开封一台全新的 MacBook Pro 到搭建区块链实例,可说是惊艳全场。在这场 1 小时 25 分中,不仅演示了转账这类基本操作,还演示了 Substrate 的一个杀手级特性——免分叉升级(forkless upgrade)。

免分叉升级

对软件增加功能、需求变动或是修复漏洞,已经如同呼吸一样自然。一般而言,不对软件升级,它很难有长的生命周期。一个致命的 Bug,或是功能不完善,都会导致被市场抛弃。

如果你是安卓系统手机用户,应该也有被 App 频繁地出现升级提醒困扰过,早期的 App 升级甚至是全量升级。升级的内容可能是加入一个新功能的入口,也可能是修复一些触发频率低到难以觉察的 bug。互联网产品奇快的迭代速度势必劝退一部分用户,或使其不再有升级意愿。因此后来出现的混合应用(Hybrid App)方案逐渐成了市场主流。这类 App 在性能需求较高的场景下运行原生代码(native code),而大多数时候我们看到的页面其实是由 WebView 处理得到的,本质和使用浏览器浏览页面一样,使用 HTML + CSS + JavaScript,这些代码都很容易动态加载,WebView 又能够动态地渲染及解释这些代码,这便促成了 App 的热升级。

如果“点一下升级按钮,再稍微等待”的升级方式已经劝退,那么区块链系统的升级可说是噩梦般的体验了。区块链系统一般是去中心化的,而且大部分更是为了安全性及性能等考量会编译到机器码。显然,去中心化的系统很难有统一的调度,更是意味着不止一个实例,这对系统升级的落实非常地不友好,就算有升级的意愿,通常伴随着替换程序及停机等糟糕体验。

试想如果区块链系统中也提供了一个可执行代码的环境,类似 WebView 可以动态加载代码并执行,那么升级将变得平滑许多。

执行器(Executor)

为了让平滑的升级变成可能,Substrate 采用了类似混合应用的解决方案。Substrate 中的执行器不仅可以执行原生代码(Native),同时提供了运行 WebAssembly(wasm) 的环境。

wasm 是一种底层(low-level)类似于汇编的代码,常见于处理浏览器中 CPU 计算密集型任务,但 wasm 已不再是只能运行在浏览器中的格式。

WasmExecutor

虽然 wasm 已经是一种低级的类汇编代码,但我们的 CPU 并不认识这种汇编,要运行 wasm 还是需要将它翻译成我们 CPU 所能执行的机器码。

最简单地,可以逐行翻译这些 wasm 代码,wasmi 便是一个由 Rust 实现的 wasm 解释器,Substrate 通过对它进行封装并暴露成 WasmExecutor。解释执行较容易实现,且执行前不需要编译的等待,非常方便用于产品原型,但它的执行效率表现并不优秀。

为了顾及执行的效率,除了 wasmi 外 Substrate 还提供了基于 wasmtime 实现的执行器,wasmtime 使用 Cranelift 支持将 wasm 编译成 CPU 可以直接执行的机器码,相对于解释执行要更加高效。

// client/executor/src/wasm_runtime.rs
pub fn create_wasm_runtime_with_code(
  wasm_method: WasmExecutionMethod,
  heap_pages: u64,
  code: &[u8],
  host_functions: Vec<&'static dyn Function>,
  allow_missing_func_imports: bool,
) -> Result<Box<dyn WasmModule>, WasmError> {
  match wasm_method {
    WasmExecutionMethod::Interpreted =>
      sc_executor_wasmi::create_runtime(...),
    #[cfg(feature = "wasmtime")]
    WasmExecutionMethod::Compiled =>
      sc_executor_wasmtime::create_runtime(...),
  }
}

执行策略

native 和 wasm 执行器是共同存在于 Substrate 中的,在执行区块时应有相应的策略。

// /primitives/state-machine/src/lib.rs
pub enum ExecutionStrategy {
  /// Execute with the native equivalent if it is compatible with the given wasm module;
  /// otherwise fall back to the wasm.
  NativeWhenPossible,
  /// Use the given wasm module.
  AlwaysWasm,
  /// Run with both the wasm and the native variant (if compatible). Report any discrepancy as an error.
  Both,
  /// First native, then if that fails or is not possible, wasm.
  NativeElseWasm,
}

Substrate 执行器拥有多种执行策略,但可以发现无论是哪种策略均与 wasm 有关,虽然 Native 几乎拥有最高的性能,但无法升级。不太精确地说,Substrate 的执行是以 wasm 为准的,虽然提供有 NativeElseWasm,但这并不推荐在执行 Runtime 中使用。

// client/executor/src/native_executor.rs
fn call<
  R: Decode + Encode + PartialEq,
  NC: FnOnce() -> result::Result<R, String> + UnwindSafe,
>(
  &self,
  ext: &mut dyn Externalities,
  runtime_code: &RuntimeCode,
  method: &str,
  data: &[u8],
  use_native: bool,
  native_call: Option<NC>,
) -> (Result<NativeOrEncoded<R>>, bool) {
  let mut used_native = false;
  let result = self.wasm.with_instance(
    runtime_code,
    ext,
    false,
    |instance, onchain_version, mut ext| {
      match (
        use_native,
        onchain_version.can_call_with(&self.native_version.runtime_version),
        native_call,
      ) {
        (_, false, _) => {
          with_externalities_safe(
            &mut **ext,
            move || instance.call(method, data).map(NativeOrEncoded::Encoded)
          )
        }
        (false, _, _) => {
          with_externalities_safe(
            &mut **ext,
            move || instance.call(method, data).map(NativeOrEncoded::Encoded)
          )
        },
        (true, true, Some(call)) => {
          used_native = true;
          let res = with_externalities_safe(&mut **ext, move || (call)())
            .and_then(|r| r
              .map(NativeOrEncoded::Native)
              .map_err(|s| Error::ApiError(s))
            );
          Ok(res)
        }
      }
    }
  );
  (result, used_native)
}

生产环境更多使用NativeWhenPossible以保证升级时的正确执行结果。

升级的方式

frame-system 是 Substrate 中一个系统级的组件,包含有 set_code 方法用于更新 RuntimeCode。可以通过发出交易直接或是简介调用这个方法。

// frame/system/src/lib.rs
pub fn set_code(origin, code: Vec<u8>) {
  ensure_root(origin)?;
  Self::can_set_code(&code)?;

  storage::unhashed::put_raw(well_known_keys::CODE, &code);
  Self::deposit_event(RawEvent::CodeUpdated);
}

当然,升级应该是合理的,我们使用 Substrate 开发的区块链可以根据需要,选择合适的升级手段。如果是中心化程度较高的场景,可以通过引入 pallet-sudo,但一般这种场景用于测试环境,更多的我们会通过治理或是更加去中心化民主投票制的方式决定是否进行升级。

总结

Substrate 提供 WasmExecutor 通过制定执行的策略以及升级的方式,让区块链升级变得平滑。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment