Ckb 加密算法调用新方案

CryptapeCryptape
4 min read

CKB 作为注重安全与可扩展性的 Nervos 底层公链,其合约执行环境对加密算法的依赖极为关键。在 CKB 合约开发中,签名、哈希、加解密等算法是常见的基础需求。传统做法通常是:

  1. 找到一个可靠的实现(通常是官方或社区推荐库);

  2. 将其静态链接引入当前项目(以源码或者静态链接的方式);

  3. 解决编译兼容、依赖冲突等问题。

但这条路径往往困难重重:

  • 语言支持不齐全:许多算法只提供了 C 语言实现,其他语言(如 JS)可能没有可用版本,或者性能极差、不适用于合约;

  • 实现质量难以保障:一些实现质量难以保障,特别是签名算法,一旦存在漏洞可能导致资产损失;

  • 依赖冲突难以解决:在集成时,可能会遇到依赖冲突等问题,处理这些问题可能需要花费大量的时间,甚至无解。

  • 实现差异导致签名失败:处理一些边缘问题不同实现可能会有差异,会导致签名失败等问题。

  • 合约代码膨胀:需要花费更多的CKB用于部署

多种语言下的集成现状

以 CKB 合约常用语言为例:

  • Rust:大多数常用算法已有较成熟实现,但依赖冲突仍时有发生。(ckb-crypto-service 本身也是用 Rust 编写)

  • C 语言:集成最复杂,小型算法可直接 #include,大型库则需通过子项目引入,依赖官方 Makefile 编译为 .o.a 后链接。

  • JavaScript:可用的第三方实现稀少,性能也乐观。我们在 ckb-js-vm 中使用C语言实现了一些常用算法,但该项目(ckb-js-vm)更新频率低,不及 ckb-crypto-service 快速支持新算法。

因此,一个统一、跨语言、低依赖的加密算法服务显得尤为必要。

基于 Spawn + IPC 的打包加密算法

CKB 通过第二次 Meepo 硬分叉,引入了spawn 多进程与进程间通信(IPC)机制,能够让合约在运行时安全地调用外部进程执行逻辑(更多见《引入多进程和进程间通信:CKB 合约开发的新方式 》) 。

在此机制的基础上,为进一步提高开发体验,我们封装了常用加密算法服务,开发了 ckb-crypto-service。它是一套基于多进程与 IPC 机制、专为合约开发构建了一套统一、安全、易用的加密算法调用方式。ckb-crypto-service 的目标不是取代所有实现,而是在合约中提供一个可靠、简洁、跨语言的加密算法基础设施,通过 IPC 直接调用,无需手动集成。

ckb-crypto-service支持以下算法的 IPC 调用:

  • Blake2b

  • SHA-256 (SHA-2)

  • RIPEMD-160

  • Secp256k1

  • Schnorr

  • Ed25519

ckb-crypto-service 的优势包括:

  • 跨语言统一:无需针对每种语言分别集成算法。例如,C 中无需再手动改写 Makefile 或引入第三方库,ckb-js-vm 中也无需将算法预集成到虚拟机;

  • 一致性:算法实现统一、经过充分测试,避免因实现差异或低质量库导致安全问题;

  • 开发体验优良:Rust 中可通过自动生成的 IPC 宏调用,使用方式与本地函数无异;C 与 JS 则只需构造请求与解析响应,比起集成原生算法库简单得多。

无论是在 Rust、C 还是 JavaScript 中,ckb-crypto-service 都能作为可靠的基础设施,简化合约中加密功能的实现,可以看作是当前 CKB 合约开发的首选加密方案

ckb-crypto-service 的多语言调用

由于 ckb-crypto-service 基于 Spawn + IPC 实现,调用起来非常简单:

Rust 调用

依赖 ckb-crypto-interfaceckb-script-ipc-common

use ckb_crypto_interface::{CkbCryptoClient, HasherType};
...

let (read_pipe, write_pipe) = spawn_cell_server(
    code_hash,
    ckb_std::ckb_types::core::ScriptHashType::Data2,
    &[CString::new("").unwrap().as_ref()],
)
.unwrap();
let crypto_cli = CkbCryptoClient::new(read_pipe, write_pipe);
let ctx = crypto_cli.hasher_new(HasherType::CkbBlake2b);
crypto_cli
    .hasher_update(ctx.clone(), crypto_info.witness.clone())
    .expect("update ckb blake2b");
let hash = crypto_cli
    .hasher_finalize(ctx)
    .expect("ckb blake2b finallize");

参考代码

JS 调用

由于 JS 暂无类似 Rust 的 ckb_script_ipc::service 自动生成工具,需手动构造 IPC 数据包并解析响应 (Example):

function runFunction(channel: Channel, payload: Object) {
  let payloadHex = new bindings.TextEncoder().encode(JSON.stringify(payload));
  let res = channel.call(new RequestPacket(payloadHex));
  if (res.errorCode() != 0) {
    throw Error(`IPC Error: ${res.errorCode()}`);
  }

  let resPayload = new bindings.TextDecoder().decode(res.payload());
  return Object.values(JSON.parse(resPayload))[0];
}

function ckbBlake2b(channel: Channel, data: number[]) {
  let hasher_ctx = runFunction(channel, { "HasherNew": { "hash_type": "CkbBlake2b" } });
  runFunction(channel, { "HasherUpdate": { "ctx": hasher_ctx, "data": data } });
  let hash = new Uint8Array(resultOk(runFunction(channel, { "HasherFinalize": { "ctx": hasher_ctx, } })));

  return hash;
}

之后再通过 IPC 的 API 创建:

function startService(): Channel {
  const args = HighLevel.loadScript().args;
  const codeHash = args.slice(35, 35 + 32);
  const [readPipe, writePipe] = spawnCellServer(codeHash, bindings.SCRIPT_HASH_TYPE_DATA2, []);
  return new Channel(readPipe, writePipe);
}

参考代码

C 调用

C 的情况与 JS 类似,并没有提供 ckb_script_ipc::service ,开发者也需要类似 JS 这样手动的构造。

这里 提供了 IPC 通讯的关键函数,开发者可以通过它来运行ckb-crypto-service

csi_init_payload(g_payload_buf, sizeof(g_payload_buf), 2);
csi_init_iobuf(g_io_buf, sizeof(g_io_buf), 2);

CSIChannel channel = {0};
err = csi_spawn_cell_server(code_hash, 1, NULL, 0, &channel);
if (err) {
    printf("failed to spawn server: %d\n", err);
    return err;
}

之后,通过csi_call发送请求:

int run_ipc_func(CSIChannel* client_channel, char* payload,
                 uint64_t payload_len, CSIResponsePacket* response) {
    CSIRequestPacket request = {0};
    request.version = 0;
    request.method_id = 0;

    request.payload_len = payload_len;
    request.payload = payload;

    int err = csi_call(client_channel, &request, response);
    if (err) {
        printf("csi_call failed, err: %d", err);
        return err;
    }

    if (response->error_code) {
        printf("csi_call response->error_code: %d", response->error_code);
        return err;
    }

    return 0;
}

这时,开发者就可以通过拼接 Payload 来调用ckb-crypto-service了:

int hasher_update(CSIChannel* channel, uint64_t ctx, uint8_t* buf,
                  uint64_t buf_len) {
    int err = 0;
    CSIResponsePacket response;
    char payload[1024];

    int offset = 0;
    offset += sprintf_(payload + offset,
                       "{ \"HasherUpdate\": { \"ctx\": %d, \"data\": [", ctx);
    for (uint64_t i = 0; i < buf_len; i++) {
        if (i == buf_len - 1)
            offset += sprintf_(payload + offset, "%u", buf[i]);
        else
            offset += sprintf_(payload + offset, "%u,", buf[i]);
    }
    offset += sprintf_(payload + offset, "] }}");
    uint64_t payload_len = offset;

    err = run_ipc_func(channel, payload, payload_len, &response);
    if (err) {
        return err;
    }

    csi_client_free_response_payload(&response);
    return 0;
}

ckb-crypto-service的 Payload 为 JSON,因为格式简单这里并没有直接使用第三方库来解析。

这里需要注意response虽然在栈中,但是其中的 Payload 则是动态分配的 , 使用完后需要调用csi_client_free_response_payload来释放资源,否则再次调用可能会失败。

之后的hasher_updatehasher_finalize 也是通过类似上述的方式调用。

完整代码

对合约体积的影响

相较于传统集成静态加密库的方式,使用 IPC 调用的一个显著优势在于对合约体积的控制

由于不同语言的调用机制存在差异,体积影响也略有不同:

  • Rust :为构造与解析 JSON Payload,引入了serdeserde_json,这是目前最通用的序列化方案。代价是合约体积大约增加 50KB ~ 60KB在资源敏感的链上环境中需要权衡。但这种体积开销是一次性的,后续可重用,也避免了集成多个算法库所带来的更大膨胀。

  • JavaScript(ckb-js-vm):由于虚拟机本身已内建 JSON 编解码能力,因此调用ckb-crypto-service不会引入额外体积,几乎 无增量开销

  • C语言:虽然也需要构造 JSON Payload,但通常通过字符串拼接实现,无需引入完整的 JSON 库,逻辑简单。整体体积增加约 4KB 左右影响极小,非常适合资源受限的合约场景。

实现细节

ckb-crypto-service 底层基于 ckb-script-ipc 实现,Server 端通过实现一个trait(可类比为接口)来定义可调用的服务方法。在 Rust 中,由于 IPC 框架支持自动生成代码,调用这些方法就像调用本地函数一样自然。而在 C 和 JavaScript 中目前还没有类似的功能,则需要手动构造 JSON 格式的请求数据并解析响应。

Payload 格式约定

每个服务进程只能实现一个 trait,因此 Payload 中不需要声明服务名或 trait 名称。虽然底层协议支持 method_id,但目前全部使用默认值 0,保持格式统一简洁。

调用时的 Payload 格式如下:

{
    "Function name (UpperCamelCase)": {
        "Arg name 1 (snake_case)": "arg 1 data",
        "Arg name 2": "arg 2 data",
    }
}
  • FunctionName 使用 UpperCamelCase(例如 HasherUpdate);

  • 参数名采用 snake_case,与 Rust 中定义一致;

  • 参数值为实际数据:如整数、字节数组等;

  • 字节数组用 [0, 1, 2, 255] 这种格式,表示 u8 数组。

响应结构

返回值也采用类似格式:

{
  "FunctionName": {
    "Ok": return_value
  }
}

{
  "FunctionName": {
    "Err": "ErrorType"
  }
}

返回值本质是 Rust 的 Result<T, E> 类型,其他语言可视为:

  • Ok:调用成功,内含函数的实际返回值(如哈希上下文 ID 或字节数组);

  • Err:调用失败,值为 CryptoError 枚举之一(如 InvalidSigVerifyFailed 等)。

trait 接口定义参考

服务接口以 trait 形式定义如下 (代码):

pub enum CryptoError {
    InvalidContext,
    InvalidSig,
    InvalidPrehash,
    InvalidRecoveryId,
    InvalidPubkey,
    RecoveryFailed,
    VerifyFailed,
}
pub struct HasherCtx(pub u64);
pub trait CkbCrypto {
    fn hasher_new(hash_type: HasherType) -> HasherCtx;
    fn hasher_update(ctx: HasherCtx, data: Vec<u8>) -> Result<(), CryptoError>;
    fn hasher_finalize(ctx: HasherCtx) -> Result<Vec<u8>, CryptoError>;

    fn secp256k1_recovery(
        prehash: Vec<u8>,
        signature: Vec<u8>,
        recovery_id: u8,
    ) -> Result<Vec<u8>, CryptoError>;

    fn secp256k1_verify(
        public_key: Vec<u8>,
        prehash: Vec<u8>,
        signature: Vec<u8>,
    ) -> Result<(), CryptoError>;

    fn schnorr_verify(
        public_key: Vec<u8>,
        prehash: Vec<u8>,
        signature: Vec<u8>,
    ) -> Result<(), CryptoError>;

    fn ed25519_verify(
        public_key: Vec<u8>,
        prehash: Vec<u8>,
        signature: Vec<u8>,
    ) -> Result<(), CryptoError>;
}

开发者可以据此推导出 Payload 的构造格式。例如:

{
  "SchnorrVerify": {
    "public_key": [2, 170, 187, ...],
    "prehash": [1, 2, 3, ...],
    "signature": [255, 1, 0, ...]
  }
}

总结

ckb-crypto-service是目前 CKB 合约开发中最推荐的加密解決方案之一。它基于 CKB 的多进程与 IPC 机制,为合约提供了一套统一、安全、易用的加密算法调用方式。相比传统集成算法库的方式,它不仅跨语言适配性更好,避免了依赖冲突和实现差异,还提升了开发效率和安全保障。无论是在 Rust、C 还是 JavaScript 中,它都能作为可靠的基础设施,简化合约中加密功能的实现。


🧑‍💻 本文作者:Han Zishuang

他是 Nervos Network L1 CKB 区块链的开发者和 DevRel

欢迎关注他更多关于 CKB 的技术分享:

0
Subscribe to my newsletter

Read articles from Cryptape directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Cryptape
Cryptape