Flow 交易流程与交易结构详解
写在前面
我们都知道在 Flow 网络中使用 Cadence 编写的智能合约也需要通过 Cadence 脚本进行查询或调用。一旦开发者握了面向资源的模型和编程理念,就很容易使用 Cadence 编写出非常具有灵活性的脚本,合约查询不支持分页?没有灵活的权限控制?多个合约接口的整合查询?想实现一个单笔交易的批量执行?这些都不是问题,通过定制灵活的 Cadence 脚本,限制程序员的就只有想象力。
同样抽象的账户设计与原生支持的多签交易也帮助我们能够安全灵活的满足各种需求场景。
为了方便大家理解,我们先从 Flow 的交易结构讲起。
Flow Cadence 交易脚本结构
交易是与 Cadence 智能合约交易的基础结构,所有针对合约的调用或者会导致合约状态进行变化的请求都是交易,而且需要不同的角色签名授权并发送给 Flow 网络中。
我们先看一个 Cadence 交易脚本的例子:
transaction(amount: UFix64) { var props: UFix64 prepare(signer1: AuthAccount, signer2: AuthAccount) { // ... self.props = amount } pre { // ... } execute { // ... } post { // ... } }
我们可以看到
transaction
包含了参数、变量和不同运行阶段的代码块,其中prepare
中也包含了参数,不过这里的参数都是AuthAccount
类型,代表着不同的授权用户(后面我们会详细说明),这里使用 prepare 代码块进行交易所需要参数和资源的初始化。使用 pre 和 post 可以进行交易执行前后的代码校验和检查,注意,虽然交易的 Cadence 脚本拥有不同执行阶段,但它仍然是原子化的,也就是说,一旦这其中任何一个代码块出现执行错误,整个交易就会回滚。
交易流程
Cadence 交易的流程如下:
- 组装数据
- 脚本
- 参数
- proposer 账户信息
- 授权人地址集合(数组形式)
- 交易费用支付人地址
- 初始化参数
- gas limit
- 授权人签名授权
- 获取 keyIndex 与 seqNumber
- encode 交易体(payload)数据并签名
- 拼装 signature 数组
- 费用支付人最终签名
- 发送交易
那么 Cadence 的交易脚本是怎么组装并发送到 Flow 区块链中的呢,我们可以从 fcl.js 的示例代码来学习:
export const sendTrx = async (CODE, args, limit = 9999) => { const txId = await fcl.send([ fcl.transaction(CODE), // Cadence 交易脚本 fcl.args(args), // transaction 参数列表 fcl.proposer(fcl.authz), // 交易发起人签名授权方法 fcl.payer(fcl.authz), // 交易费支付者签名授权方法 fcl.authorizations([fcl.authz]), // 这里是 prepare 的授权账户的签名(会转换为 AuthAccount 参数) fcl.limit(limit) // gas limit ]).then(fcl.decode) return txId }
以上代码片段中需要注意的点是:
- proposer 是交易的发起人(只做链下验证)
- payer 是交易费用的支付人,需要最后签名
- authorizations 则为数组形式,能够支持多个签名,并且会在 Cadence 脚本中以 参数的形式传递到
prepare
代码块的参数中
- args 也是列表类型,会成为 transaction 代码块的参数
- fcl.authz 就是签名的函数,可以支持远程签名(托管)与本地签名(非托管),会将整个交易体 encode 之后签名交易体的 hash 值,并将其拼装在 payloadSigs 数组上完成多签中某个账号的授权
流程明确之后,接下来我们通过交易体数据更直观的了解交易
交易体详解
这里引用官方的一张交易体解剖图
我们也可以结合数据来看 raw transactions :
{ script: 'Cadence script ...', // Cadence 交易脚本 refBlock: 'd719c892c80b45de3d19c459dc7eeba8ab809b5f3b007a51c69f697b7eb42361', // 引用的区块 ID arguments: [ { type: 'String', value: 'Test' } ], // Cadence 交易的参数 proposalKey: { address: '0xe242ccfb4b8ea3e2', keyId: 0, sequenceNum: 480 }, // 交易发起人的信息 payer: '0xc6de0d94160377cd', // 交易费用支付人信息 authorizers: [ '0xe242ccfb4b8ea3e2', '0xc6de0d94160377cd', '0xb05b2abb42335e88' ], gasLimit: 1000, payloadSignatures: [ { address: '0xe242ccfb4b8ea3e2', keyIndex: 0, signature: '9aa7ca757c0c995d1cc11b71c6c5d3b83be92708905c8ba4528fadc602438acd027b479e1546f9ed96a3c9f8df424eccd318333c7f55378ce4994b164972c450' },{...},{...} ], envelopeSignatures: [ { "address": "0x01", "keyId": 1, "sig": "0xabc123" } ] }
这个 Json 格式的数据就是我们签名之后的交易体:
- script —— 之前提到的 Cadence 脚本
- refBlock —— 交易构建时的区块 id
- arguments —— Cadence 的交易参数(transaction 方法体的参数)会根据参数的顺序和类型进行校验
- proposalKey —— 交易发起人的信息
- address —— 地址
- keyId —— 公钥对应在账户中的 Index,这里牵涉到 Flow 抽象的账户模型设计详情参考这里
- sequenceNum —— key 的交易序号
- authorizers —— 授权人的地址,这里和 Cadence 中的 prepare 参数匹配,不要求顺序一致,但不能缺少任意一方的签名。
- gasLimit —— 交易最大的 gas 消耗值
- payloadSignatures —— 针对上述授权的签名信息列表,用来判断是否符合多签权限的签名集合
- address —— 授权人地址
- keyIndex —— 授权人 key 索引
- signature —— 授权私钥签名
- envelopeSignatures —— 手续费支付人,最后签名封装交易(正常的流程是用户自身会成为 proposal 和 payer)
但也可以借助一些托管服务或工具通过 Raw 交易传递的形式让别人代付交易费
交易手续费代付
根据上述图解和流程来看,Flow 在这里设计的优势在于,payer 作为最后一个封装并签名交易的流程是最安全的,用链的校验功能解决了信任的问题。
作为授权交易的用户只需要费用代付的人付款,并避免代付的人通过交易数据篡改授权人授权的脚本而窃取授权人的资产,同理交易费用代付的人也有需要校验交易的正确性,同时也不需要花费大力气去校验交易来源的可靠性,是否存在中间人攻击等问题,只做交易核准和最后原始交易的封装签名。鉴权和安全性都由 Flow 网络保证。
从业务上来讲,授权发起交易的用户和交易代付的人可以在此机制下完全互不信任,也能完成手续费代付的需求。这其中是由 Flow 抽象的账户模型和合理的交易签名机制来保障整个流程的安全性。
具体的签名代付流程其实在交易流程中已经体现出来,用户对 payload 进行的交易体进行签名并将交易数据发送给代付服务,代付服务校验用户签名和交易内容,无误之后对 envelope 进行签名,相当于授权进行手续费代付的操作,并将最终的交易发送到 Flow 区块链中,完成手续费代付的操作。
我们熟知的 Blocto 服务就是通过这样的形式在托管服务端完成了类似于传递签名的功能,帮助用户进行手续费的补贴。
我们可以在区块浏览器中看到这笔交易中 payer 是一个第三方地址,交易的发起和授权则是由用户完成的。
总结
灵活的脚本加合理设计的交易结构,能够帮开发者满足各种场景下的需求。
下一篇文章我会结合实际的使用和场景来分享一下 Flowns 如何通过灵活的 Cadence 脚本和多重签名完成超级管理员账户的权限分离,让 Flow Cadence 能够做到不修改现有的合约权限结构但也可以安全便捷的实现多方签名授权的交易用例。