Ckb 加密算法调用新方案


CKB 作为注重安全与可扩展性的 Nervos 底层公链,其合约执行环境对加密算法的依赖极为关键。在 CKB 合约开发中,签名、哈希、加解密等算法是常见的基础需求。传统做法通常是:
找到一个可靠的实现(通常是官方或社区推荐库);
将其静态链接引入当前项目(以源码或者静态链接的方式);
解决编译兼容、依赖冲突等问题。
但这条路径往往困难重重:
语言支持不齐全:许多算法只提供了 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-interface 和 ckb-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_update
和 hasher_finalize
也是通过类似上述的方式调用。
对合约体积的影响
相较于传统集成静态加密库的方式,使用 IPC 调用的一个显著优势在于对合约体积的控制。
由于不同语言的调用机制存在差异,体积影响也略有不同:
Rust :为构造与解析 JSON Payload,引入了
serde
与serde_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
枚举之一(如InvalidSig
、VerifyFailed
等)。
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 的技术分享:
Subscribe to my newsletter
Read articles from Cryptape directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
