这篇文章上次修改于 523 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

1 基础

Cell 的组成

  • capacity:cell 的容量,同时也代表该 cell 有多少 CKB
  • lock:一般用来说明该 cell 归谁所有;一般只验证 inputs
  • type:一般用来存放业务合约;一般既验证 inputs、也验证 outputs
  • data:存放数据

交易

销毁一组 cell、生成一组新的 cell

交易的组成

let tx = TransactionBuilder::default()
        .inputs(inputs)
        .outputs(outputs)
        .outputs_data(outputs_data.pack())
        .cell_deps(cell_deps)
        .witnesses(witnesses.pack())
        .build();
  • inputs:存放对已有的 live cell 的引用
  • outputs:生成的新 cell
  • outputs_data:存放数据,位置与 outputs 中的 cell 一一对应
  • cell_deps:存放交易所需要的合约或需要读取的 cell 的引用
  • witnesses:可以简单理解为存放解锁 cell 所需的信息。有时候也可以存放其它数据,相对于 outputs_data 来说,witness 不会占用 cell 的体积,它是不需要 capacity 的。

交易的类型

  • 生成新的 cell:inputs 中有,outputs 中没有
  • 更新 cell:inputs 和 outputs 中都有
  • 销毁 cell:inputs 中有,outputs 中没有

交易签名 sign

可以简单理解为在 witness 中存放可以解锁 cell 的信息。

sign 仅仅是将有效信息替换 witness placeholder。

一般情况下,一个 witness 对应一组 lock 相同的 cell,更为复杂的情况需要看该文档:How to sign transaction

交易手续费 tx fee

sum(tx.inputs.cells.capacity) - sum(tx.outputs.cells.capacity)

为什么签名前需要填充 witness placeholder

  • 原因一:是涉及交易手续费的计算,交易手续费与 tx size 有关:

    const KB: u64 = 1000;
    const FEE_RATE: u64 = 1000;
    
    fn fee(tx_size: usize) -> Capacity {
        let fee = FEE_RATE.saturating_mul(tx_size as u64) / KB;
        Capacity::shannons(fee)
    }

    而 tx size 与 witness placeholder 有关,所以在 balance tx 前,需要先填充 witness placeholder。

    不过可以通过多在 inputs 中收集 1 CKB 避免这个问题。

  • 原因二:为了安全,需要确保签名前后交易的的大小不发生改变。

为什么需要 balance tx

从交易手续费的计算中可以看出,如果 sum(tx.inputs.cells.capacity) 非常大的话,多出来的 capacity 就相当于凭空消失了,也就是用户会损失一笔 CKB。

2 Tx

fee 与 tx size 有关:

const KB: u64 = 1000;
const FEE_RATE: u64 = 1000;

fn fee(tx_size: usize) -> Capacity {
    let fee = FEE_RATE.saturating_mul(tx_size as u64) / KB;
    Capacity::shannons(fee)
}

tx size 与 witness placeholder 有关,所以在 balance tx 前,需要先填充 witness placeholder。

填充 witness:How to sign transaction

3 Lock

privkey -> pubkey -> keccak160(pubkey) -> omni lock args 的一部分
privkey -> pubkey -> ckb_blake2b(pubkey)[0..20] -> sighash lock args

4 Witness

假设以下 cells 如无特别提及,lock script 都是 A,也就是属于同一个人。

inputs:
  - inputs[0~2] 是普通的 ckb cells,收集用来支付 outputs' capacity 和 fee
outputs:
  - outputs[0] 是一个带有 data 的 ckb cell,
  - outputs[1] 是一个需要验证 outputType 的 type script cell
  - outputs[2] 是找零 ckb cell

在构建交易时,因为 inputs[0~2] 都属于同一个 lock script,所以可以算作一个 input group。
因此在签名阶段,生成 message 时,可以只对 witnesses[0].lock 填充 65-byte placeholder 即可,然后对 inputs[0] 生成 message。

在签名阶段,outputs[1] 需要验证 WitnessArgs.outputType,而它对应的 witnesses[1] 也属于 input group 的范围。
而由于对 input[0] 生成 message 的时候,会把 input group 内的每个 witness 和 witness length 都 hash 入其中,在顺序上,需要:
签名并填充 witnesses[1].outputType
然后再签名并填充 witnesses[0].lock

否则,两端生成的 message 应该会不一样,即便是给 witnesses[1].outputType 填充 placeholder,也无效。

附带 context,生成 Secp256k1 message 的流程:https://github.com/nervosnetwork/ckb-system-scripts/wiki/How-to-sign-transaction#p2pkh

  1. hasher tx hash
  2. hash first witness (and its length) in the group, with 65-byte placeholder in witness.lock
  3. hash each witness (and its length) in the group
  4. hash each witness (and its length) not in any group

5 Omni Supply

Omni Supply Mode

如果 sudt.type.args 是 omni supply cell lock 的 hash,其实 sudt 的 owner 就是 omni supply cell,这样的就已经满足了 inputs 中有一个 cell 是 owner 的条件。

6 合约内存不足

交易报错:
jsonrpc output failure Failure { jsonrpc: Some(V2), error: Error { code: ServerError(-302), message: "TransactionFailedToVerify: Verification failed Script(TransactionScriptError { source: Inputs[0].Lock, cause: VM Internal Error: MemWriteOnExecutablePage })", data: Some(String("Verification(Error { kind: Script, inner: TransactionScriptError { source: Inputs[0].Lock, cause: VM Internal Error: MemWriteOnExecutablePage } })")) }, id: Num(0) }

原因:合约太大,load_script() 报错

解决:

// Alloc 4K fast HEAP + 2M HEAP to receives PrefilledData
default_alloc!(4 * 1024, 2048 * 1024, 64);