从 omnilock 讲起(二)

上一篇,简单介绍了一下 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。

4 Likes