现有的CKB在编写TS(TypeScript)时,因为TS中可能涉及到很多方法,通常会有两种方式来判断当前TS是处理哪种方法,1)是判断交易的结构是否满足某种形式,2)通过在Witness里添加方法名。
这两种方法都有弊端,方式1的弊端是,如果业务特别复杂,可能会存在两种业务的结构都是类似的,这种情况下单纯用方式一可能就无法处理,还需要增加另外的IF-Condition来判断,代码最后都比较复杂,也比较丑陋难懂。
方法2的处理方式,比较取巧,但是witness本意也不是用来存方法名的,而且存在被替换的风险。
这两种方法都存在一个比较大的缺点,就是在通过浏览器去查看交易时,从用户和浏览器角度讲都很难判断这个交易是做什么的。对于以太坊可以轻松的识别这个交易的意图,但是对于CKB的XUDT合约,用户需要仔细验证交易数据来判断,对于开发者和普通用户都很不友好。

这里的区别在于eth的交易中包含了交易方法名,而UTXO的交易中没有。CKB是否也可以做到呢?在实践中,我们观察到,TS的代码往往都是IF-Condition结构的,就是IF是某个Action,然后判断是否满足条件1,2,3。对于其他的Action在当前交易执行时,实际上并没有执行到这些代码,真正被执行的只有这一部分。我们考虑是否可以将每个Action提取为一个单独的TS呢?而多个Action共用一个状态。这样设计带来最大的几个好处:
1)不需要匹配交易结构或者使用Witness。
2)一个TS只负责一个Action,职责单一。
3)浏览器和用户可以识别交易。
经过实践后,是可行的。这里我们简单描述下我们这种编程模式的用法。我们将TS分为Action TS和State TS。State TS负责存储状态数据。Action TS没有状态可以随时生成,然后和State TS配合使用。
对于状态TS主要定义数据的存储格式,以及它可以和哪些Action TS共同执行,在状态TS中Args中存储了它允许的Action script 的script hash。状态TS需要满足条件:
1)在Inputs或Outputs中只有且必须只有一个对应的Action Script。
在Action Script中,它的Script args中需要传入状态TS的script hash,需要满足:
1) 如果Inputs/Outputs(这里要看具体情况是哪个)中没有状态TS则不执行任何操作,直接返还成功0。这个条件要在最前面进行执行。
在实际使用时,我们允许凭空产生或销毁一个Action Script Cell,例如Input是一个CKB Cell,然后Output就是这个Action Script,这个Action Script是无状态的,它没有Cell Data。也可以配合状态合约,在Output中产生一个Action Script。
Action Script Cell可以和状态共同使用,举一个最简单的例子,转账,我们的转账要求Input的总额 = Output,即转帐和销毁的逻辑不混合在一起。Input1 是A的状态cell,input2是转账Acton Script,Outputs是B的状态Cell,和A的状态Cell。输出中我们可以根据情况继续保留这个Action Script,这样可以复用。
交易逻辑容易搞混是常见的ckb 编程的一个弊端,因为合约中多个Action Script的逻辑混合在一起,有时候交易执行成功了,但是可能并不是我们想要的。比如A给B转账100,可能在交易构造时,不小心忘记找零了,就会导致A的余额凭空消失了。实际上是同时执行了转账和销毁。
经过实践我们发现这种模式带来的好处还有:
1)验证更严格。(因为不会存在几种不同的Action 混在一起,明确的执行目的)
2)单个Action script的脚本代码量非常少,代码更清晰,逻辑更简单,也更容易测试。项目逻辑越复杂,这个优点越明显。
3)通过浏览器很容易判断这个交易是什么交易,只要看Action script即可。
细心的朋友可能发现一个问题,在我们的设计中,两个类型的Script 的args都有对方的script hash,这个如何做到呢?
这里的解决方案是,使用TypeID,先准备好生成所有脚本的TypeID的Input,然后计算出TypeID Hash,由于TypeID可以预先计算好,这个时候再将TypeID硬编码到所有的Script代码中,而不是放到参数中,这样在每一份合约实例实际上使用的不同的Script 代码,原来的参数已经硬编码到代码中,所以可以直接部署。
另外一种方式是将其中一种Script args里的script hash换成code hash,这样的话,先生成其中一个A Script,其参数中使用的是其他Script 的code hash,然后计算出script hash,作为另外一个B Script中的参数。这种情况存在被攻击的风险,但是可以在测试的时候使用,方便编写测试代码。
4 Likes
每个 action 用独立的 script 的确是一种做法,这个没有问题,使用一个 script 包含多个 action,以及每个 script 对应一个 action,其实都是 CKB 上可以实现的做法,具体使用哪种,会取决于具体团队。
我在这里想聊的是另一个点:
2)通过在Witness里添加方法名。
方法2的处理方式,比较取巧,但是witness本意也不是用来存方法名的,而且存在被替换的风险
首先,我觉得没法得出 “witness 本意不是用来存方法名的”。以常用的 WitnessArgs 结构来举例,WitnessArgs 中实际上包含 lock, input_type 和 output_type 三个字段。其中 input_type 和 output_type 字段就是留给 typescript 合约来存放参数信息的(具体使用哪一个,取决于 typescript 出现在 input cell 还是 output cell 之中),那么其实在 input_type 中存放方法名,或是类似 Ethereum 中 hash 过后的 32bit 长的 method id,都是完全没问题的。同时 input_type 以及 output_type 中的内容均会被 lock 中包含的签名所覆盖,所以也没有替换的风险。
我并不是说使用方法名比拆分多个 script 更好。我只想指出这两种方式都没有本质上的问题,更多的还是取决于实际偏好。
就是在通过浏览器去查看交易时,从用户和浏览器角度讲都很难判断这个交易是做什么的
我个人认为这里取决于工具层,比如浏览器中,选择引入哪种规范。浏览器可以引入对方法名 / method ID 的解析,也可以维护 script → action 的 mapping。无论哪一种,只要确定规范,引入规范,浏览器端都应该可以解析出一个交易实际完成的 action。只是这件事情历史上一直没有推进。
1 Like
貌似是不会覆盖的,见这个交易:
其中唯一的WitnessArgs解析结果是这样(InputType是Undefined)
{
"lock": "0x02090b131709a41009a1b64f90e885940dd0d0902e6b7c2b5e4152b7cf78405b455594d56a223b86df01089f4412086f03cbc1cc6ad1b5f167ac4c3d6af90c8b01",
"output_type": "0x5a000000140000003d0000005a0000005a00000025000000434b426f6f7374416368696576656d656e742e7570646174655f616368696576656d656e741d00000008000000150000000c0000000d000000020400000000000000"
}
lock的部分是Input Lock的签名,
而其中output_type解析结果是这样
{
"method_path": "0x434b426f6f7374416368696576656d656e742e7570646174655f616368696576656d656e74",
"arguments": [
{
"arg_type": "0x02",
"data": "0x00000000"
}
]
}
相关的Schema是这样:
table TransactionRecipe {
method_path: Bytes, // The method path (e.g., b"UDT.transfer", b"AMM.swap")
arguments: RecipeArgumentVec, // Structured arguments for the method
cell_deps: CellDepVecOpt, // Optional cell dependencies
header_deps: Byte32VecOpt, // Optional header dependencies
}
// Recipe argument that can contain either inline data or a reference to data
table RecipeArgument {
arg_type: byte, // 0 = inline_data
// 1 = input_data_reference (index into inputs for cell data) - matches Source::Input
// 2 = output_data_reference (index into outputs_data) - matches Source::Output
// 3 = cell_dep_data_reference (index into cell_deps for dep cell data) - matches Source::CellDep
// 4 = header_reference (index into header_deps) - matches Source::HeaderDep
data: Bytes, // For inline_data: the actual data
// For references: 4-byte LE index into the corresponding vector
}
vector RecipeArgumentVec <RecipeArgument>;
成功交易之后这段Witness也并没有完全覆盖掉
1 Like