上一篇,简单介绍了一下 omnilock 的诞生背景以及复杂性。这一篇还是介绍前置内容,同时给 omnilock 的解锁流程打下一个简单的轮廓基础。我们从一般交易的构建流程开始讲起。
一般交易的验证流程
首先,我们要理解一般意义上的交易验证是怎么回事,以 ckb 默认的 secp256k1_black2b_hashall 为例,它的验证过程是在 vm 中重新构建 message,然后用交易中提供的 signature 和构建的 message 恢复出公钥,与 script 上的 args 进行对比,如果一致,即通过验证,如果不一致,该交易非法。
在区块链世界中,大部分的交易验证流程从本质上来说,就是如此,不同的链可能有不同的算法,比如 ed25519 和 secp256k1 的区别,或者是 message 生成时,hash 算法的区别,比如 sha256 与 keccak256,这里有历史原因也有巧合因素。
在 ckb 上,lock script 是可以随意添加的,也就是说,任意算法在 ckb 上支持都是有可能性的,而 omnilock 支持多种算法就是一个案例,从理论上说,如果 ckb 上一个 lock script 的验证逻辑和 message 生成的逻辑与其他链一致,那么这些链的钱包,都可以被伪装成 ckb 的钱包使用,这也是 ckb 灵活性带来的可能性,omnilock 的诞生也与之相关。
omnilock 的 message 生成
任何 lock script 首先考虑的不是花里胡哨的验证过程,而是怎么保证它的安全性,lock 的设计需要慎重考虑这一点。所谓的安全性,是指非本人(本私钥)意愿下,该 lock 无法被任何人解开或者篡改。
ckb 上的交易验证比上面说的更复杂一点,因为它是以 script group 为最小单位进行验证的,关于这一点可以看之前写的文章 —— CKB VM Verification Rules,一个交易可以有多个 script group 同时存在,不同的 script group 几乎不相互干扰。
作为签名的 message,一般意义上来说,需要将交易中需要完全确定,不能篡改的地方在生成的时候完全覆盖掉,这样验证的过程就保证了它的一致性,因为 message 的生成就是将需要的数据 hash 的过程,message 一致即 hash 值一致。
在 ckb 上构建交易的过程,一般如下:
- 将交易需要销毁的 input 和生成的 output 塞入 transaction
- 将解锁过程中需要用到的算法 cell 作为 dep cell 塞入 transaction
- 将解锁过程中需要用到的 header 作为 header dep 塞入 transaction
- 将 witness 中 signature 位置用全零填充
- 将 change cell 放入 transaction 并确定手续费
这样,一个未签名但完整的 transaction 就构建完成了。接下来就是按照 script group 生成对应的 message 并签名,然后替换之前用全零填充的 signature 位。
一般意义上,保证交易不被篡改的 message 生成需要这些数据,按顺序填充至 hash 算法中:
-
transaction hash
-
同 script group 下,第一个 witness 的长度和内容(signature 位置是全零填充的)
-
同 script group 下,除第一个 witness 以外的所有其他 witness 的长度和内容
-
超出 input 数量的 witness 的长度和内容
用代码来描述就是:
pub fn generate_message(
tx: &TransactionView,
script_group: &ScriptGroup,
zero_lock: Bytes,
) -> Result<Bytes, ScriptSignError> {
if tx.witnesses().item_count() <= script_group.input_indices[0] {
return Err(ScriptSignError::WitnessNotEnough);
}
let witnesses: Vec<packed::Bytes> = tx.witnesses().into_iter().collect();
let witness_data = witnesses[script_group.input_indices[0]].raw_data();
let mut init_witness = if witness_data.is_empty() {
WitnessArgs::default()
} else {
WitnessArgs::from_slice(witness_data.as_ref())?
};
init_witness = init_witness
.as_builder()
.lock(Some(zero_lock).pack())
.build();
// Other witnesses in current script group
let other_witnesses: Vec<([u8; 8], Bytes)> = script_group
.input_indices
.iter()
.skip(1)
.filter_map(|idx| witnesses.get(*idx))
.map(|witness| {
(
(witness.item_count() as u64).to_le_bytes(),
witness.raw_data(),
)
})
.collect();
// The witnesses not covered by any inputs
let outter_witnesses: Vec<([u8; 8], Bytes)> = if tx.inputs().len() < witnesses.len() {
witnesses[tx.inputs().len()..witnesses.len()]
.iter()
.map(|witness| {
(
(witness.item_count() as u64).to_le_bytes(),
witness.raw_data(),
)
})
.collect()
} else {
Default::default()
};
let mut blake2b = new_blake2b();
blake2b.update(tx.hash().as_slice());
blake2b.update(&(init_witness.as_bytes().len() as u64).to_le_bytes());
blake2b.update(&init_witness.as_bytes());
for (len_le, data) in other_witnesses {
blake2b.update(&len_le);
blake2b.update(&data);
}
for (len_le, data) in outter_witnesses {
blake2b.update(&len_le);
blake2b.update(&data);
}
let mut message = vec![0u8; 32];
blake2b.finalize(&mut message);
Ok(Bytes::from(message))
}
要注意的是,该 blake2b 算法的 personal 字段是 b"ckb-default-hash"
。
由于 omnilock 支持 cobuild,这种模式下,message 生成的规则有一定的不同,它需要的字段也不同,具体而言:
- 根据 cobuild 的 message 信息存在与否,有生成不同 personal 的 bloack2b,并塞入 message 信息
- transaction hash
- 所有 input cell 的内容(lock/type)、长度、数据
- 超出 input 数量的 witness 的长度和内容
用代码描述就是:
pub fn cobuild_generate_signing_message_hash(
message: &Option<bytes::Bytes>,
tx_dep_provider: &dyn TransactionDependencyProvider,
tx: &TransactionView,
) -> Bytes {
// message
let mut hasher = match message {
Some(m) => {
let mut hasher = new_sighash_all_blake2b();
hasher.update(m);
hasher
}
None => new_sighash_all_only_blake2b(),
};
// tx hash
hasher.update(tx.hash().as_slice());
// inputs cell and data
let inputs_len = tx.inputs().len();
for i in 0..inputs_len {
let input_cell = tx.inputs().get(i).unwrap();
let input_cell_out_point = input_cell.previous_output();
let (input_cell, input_cell_data) = (
tx_dep_provider.get_cell(&input_cell_out_point).unwrap(),
tx_dep_provider
.get_cell_data(&input_cell_out_point)
.unwrap(),
);
hasher.update(input_cell.as_slice());
hasher.update(&(input_cell_data.len() as u32).to_le_bytes());
hasher.update(&input_cell_data);
}
// extra witnesses
for witness in tx.witnesses().into_iter().skip(inputs_len) {
hasher.update(&(witness.len() as u32).to_le_bytes());
hasher.update(&witness.raw_data());
}
let mut result = vec![0u8; 32];
hasher.finalize(&mut result);
Bytes::from(result)
}
如果 cobuild 的 message 存在,blake2b 算法的 personal 是 b"ckb-tcob-sighash"
,否则是 b"ckb-tcob-sgohash"
。
小结
再次强调一下,如果一个交易验证的 message 一致,算法一致,那么它就可以用对应链的钱包进行签名操作,钱包的功能从理论上来说,最核心的地方就是对一段 message 签名并生成 signature。