交易
Neo区块去掉区块头部分就是一串交易构成的区块主体,因而交易是整个Neo系统的基础部件。钱包、智能合约、账户和交易相互作用但最终都转化成交易被记入区块链中。在Neo的P2P网络传输中,信息被打包成 InvPayload
信息包来传送(Inv即Inventory)。不同信息包有自己需要的特定数据, InventoryType.Tx
用于标识网络中的InvPayload信息包内装的是交易数据。
数据结构
在Neo网络中,交易的数据结构如下所示:
字段 | 类型 | 说明 |
---|---|---|
version | byte | 交易版本号,目前为0 |
nonce | uint | 随机数 |
sysfee | long | 支付给网络的资源费用 |
netfee | long | 支付给验证人打包交易的费用 |
validUntilBlock | uint | 交易的有效期 |
signers | Signer[] | 发送方以及限制签名的作用范围 |
attributes | TransactionAttribute[] | 交易所具备的额外特性 |
script | byte[] | 交易的合约脚本 |
witnesses | Witness[] | 用于验证交易的脚本列表 |
version
version属性允许对交易结构进行更新,使其具有向后兼容性。 目前版本为0。
signers
signers中的第一个字段为交易发起账户的地址哈希。由于Neo N3弃用了UTXO模型,仅保留有账户余额模型,原生资产NEO和GAS的转账交易统一为NEP-17资产操作方式,因此交易结构中不再记录inputs和outputs字段,通过signers字段来跟踪交易的发送方。
signers中余下的字段定义了签名的作用范围。当 checkwitness 用于交易验证时,除交易发送者 sender 外,其他的 signers 都需要定义其签名的作用范围。详情请参见 签名作用域 。
字段 | 说明 | 类型 |
---|---|---|
Account | 账户脚本哈希 | UInt160 |
Scopes | 指定签名的作用范围 | WitnessScope |
AllowedContracts | 签名可验证的合约脚本数组 | UInt160[] |
AllowedGroups | 签名可验证的合约组公钥 | ECPoint[] |
Rules | 当 scopes 设置为 WitnessRules 时,填写签名规则数组 | WitnessRules[] |
sysfee
系统费用取决于交易脚本的大小,数量和NeoVM指令类型。每一个指令所对应的费用,请参考 opcode 费用 。Neo N3取消了每笔交易10 GAS的免费额度,系统费用总额受合约脚本的指令数量和指令类型影响。计算公式如下所示:
其中,OpcodeSet为指令集,𝑂𝑝𝑐𝑜𝑑𝑒𝑃𝑟𝑖𝑐𝑒𝑖为第i种指令的费用,𝑛𝑖为第i种指令在合约脚本中的执行次数。
netfee
网络费是用户向Neo网络提交交易时支付的费用,作为共识节点的出块奖励。每笔交易的网络费存在一个基础值,用户支付的网络费需要大于或等于此基础值,否则交易无法通过验证。基础网络费计算公式如下所示:
其中,VerificationCost为虚拟机验证交易签名执行的指令相对应的费用,tx.Length为交易数据的字节长度,FeePerByte为交易每字节的费用,目前为0.00001GAS。
attributes
根据具体的交易类型允许向交易添加额外的属性。 对于每个属性,必须指定使用类型,以及外部数据和外部数据的大小。
每个交易最多可以添加16个属性。
script
在NeoVM上执行的脚本,决定了交易的效果。
witnesses
witnesses属性用于验证交易的有效性和完整性。Witness即“见证人”, 包含两个属性。
字段 | 说明 |
---|---|
InvocationScript | 执行脚本,向验证脚本传递参数 |
VerificationScript | 验证脚本 |
可以为每个交易添加多个见证人,也可以使用具有多方签名的见证人。
执行脚本
执行脚本可以通过
0x0C(PUSHDATA1) + 0x40(数据长度为64字节) + 签名
添加签名。重复此步,可以为多方签名合约推送多个签名。
验证脚本
验证脚本,常见为地址脚本,包括普通地址脚本和多签地址脚本,该地址脚本可以从钱包账户中直接获取,其构造方式,请参考 钱包-地址 ;
也可以为自定义的鉴权合约脚本。
交易序列化
除IP地址和端口号外,Neo中所有变长的整数类型都使用小端序存储。交易序列化时将按以下字段顺序执行序列化操作:
字段 | 说明 |
---|---|
version | - |
nonce | - |
systemFee | - |
networkFee | - |
validUntilBlock | - |
signers | 需先序列化数组长度 WriteVarInt(length) ,之后再分别序列化数组各个元素 |
attributes | 需先序列化数组长度 WriteVarInt(length) ,之后再分别序列化数组各个元素 |
script | 需先序列化数组长度 WriteVarInt(length) ,之后再序列化字节数组 |
witnesses | 需先序列化数组长度 WriteVarInt(length) 之后再分别序列化数组各个元素 |
WriteVarInt(value) 是根据value的值,存储非定长类型, 根据取值范围决定存储大小。
Value 值范围 | 存储类型 |
---|---|
value < 0xFD | byte(value) |
value <= 0xFFFF | 0xFD + ushort(value) |
value <= 0xFFFFFFFF | 0xFE + uint(value) |
value > 0xFFFFFFFF | 0xFF + value |
交易签名
交易签名是对交易本身的数据(不包含签名数据,即witnesses部分)进行ECDSA方法签名,然后填入交易体中的 witnesses
。
交易JSON格式示例,其中script与witnesses字段使用Base64替代原有的Hexstring编码:
签名作用域
在 Neo Legacy 中,交易签名是全局有效的,所有合约均可使用用户签名。为了让用户能更细粒度地控制签名的作用范围,Neo N3 新增了签名作用域(WitnessScope),对交易结构中的 signers 字段进行了变更,可实现签名只限于验证指定合约的功能,防止未经授权的合约随意使用用户签名。
Scopes
在构造交易时,需要指定 signers
参数中的 scopes
字段,其定义了签名的作用范围,包含的类型如下表所示。
值 | 名称 | 说明 |
---|---|---|
0x00 | None | 仅对交易签名,不允许任何合约使用该签名 |
0x01 | CalledByEntry | 签名只限于由 Entry 脚本调用的合约脚本,建议作为钱包的默认签名作用。 |
0x10 | CustomContracts | 自定义合约,在指定的合约中可以使用该签名。可与 CalledByEntry 配合使用。 |
0x20 | CustomGroups | 自定义合约组,在指定的合约组中可以使用该签名。可与 CalledByEntry 配合使用。 |
0x80 | Global | 签名全局有效,所有合约均可使用签名。风险极高,合约有可能会转移地址中的所有资产,仅在极其信任该合约时选择。 |
0x40 | WitnessRules | 用户自己指定验签规则和范围,参见 WitnessRule 。 |
为了更好地说明各类型,我们假设一条合约调用链为: [entry]->[合约A]->[合约B]->[合约C]...->[Target]
Target 合约调用 CheckWitness 验签,且用户赋予了签名。当 scopes 分别设置为以下值时,验签情况如下:
-
None
- Target 合约位于任何位置时都不允许验签通过 -
Global
- Target 合约位于任何位置时都允许验签通过 -
CallByEntry
- Target 合约位于 entry 或 合约A 时才允许验签通过 -
CustomContracts
- 此时用户需要额外指定一个合约列表 CustomContracts ,仅当 Target 合约属于 CustomContracts 中时允许验签通过 -
CustomGroups
- 此时用户需要额外指定一个 Group 公钥列表 CustomGroups ,仅当 Target 合约带有 CustomGroups 中任意一个公钥认证时,允许验签通过
WitnessRule
Action(Allow|Deny) 和 Condition (判断条件)
执行逻辑:先执行判断条件,如果符合,则返回Action,返回Allow代表验签成功,Deny代表验签失败。
WitnessCondition 判断条件
-
Boolean:true|false
“expression” = <bool>
-
Not: 逻辑非,对其它条件求反
“expression”=<Condition>
-
And:逻辑与,连接其它条件求与
“expressions”=<Condition[]>
-
Or:逻辑或,连接其它条件求或
“expressions”=<Condition[]>
-
ScriptHash:验证当前合约hash是否匹配,相当于 CustomContracts
“hash”= <UInt160>
-
Group:验证当前合约的公钥是否匹配,相当于 CustomGroups
“group”=<ECPoint>
-
CalledByEntry:验证当前合约是否为entry调用,相当于 CallByEntry
-
CalledByContract:验证当前合约的上一级合约hash是否匹配
“hash”=<UInt160>
-
CalledByGroup:验证当前合约的上一级合约公钥是否匹配
“group”=<UInt160>
示例
该字段目前只能通过SDK构造交易时定义。为了帮助理解,可参考 JSON 格式的代码示例: