这篇文章上次修改于 544 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
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
- hasher tx hash
- hash first witness (and its length) in the group, with 65-byte placeholder in witness.lock
- hash each witness (and its length) in the group
- hash each witness (and its length) not in any group
5 Omni Supply
如果 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);
没有评论