在网络结构上,NEO 采用点对点网络结构,并使用 TCP 协议进行通讯。+ v9 _& P! _0 ~8 u) b# ^: r% Y1 e
网络中存在两种节点类型,分别是普通节点和共识节点。普通节点可以广播、接收和转发交易、区块等,而共识节点可以创建区块。
NEO 的网络协议规范与比特币的协议大致类似,但在区块、交易等的数据结构上有很大的不同。; o( \2 I4 t& n: F/ X" M. B m1 V# a
约定5 {. g/ k, `, E$ W$ A9 H* [' T
字节序
NEO 系统中所有的整数类型都是采用小端序 (Little Endian) 编码,只有 IP 地址和端口号采用大端序 (Big Endian) 编码。2 y- K4 L/ k1 V7 i7 p
散列- ^2 R6 `- J3 f4 p1 w
NEO 系统中会用到 2 种不同的散列函数:SHA256 和 RIPEMD160。前者用于生成较长的散列值,而后者用于生成较短的散列值。通常生成一个对象的散列值时,会运用两次散列函数,例如要生成区块或交易的散列时,会计算两次 SHA256;生成合约地址时,会先计算脚本的 SHA256 散列,然后再计算上一个散列的 RIPEMD160 散列。8 Q2 }: v4 P a2 s6 u" W
此外,区块中还会用到一种散列树 (Merkle Tree) 的结构,它将每一笔交易的散列两两相接后再计算一次散列,并重复以上过程直到只剩下一个根散列 (Merkle Root)。! n4 l) r; v+ [' F( ~7 T. T+ C
变长类型
varint:变长整数,可以根据表达的值进行编码以节省空间。4 |: w/ j. c; i' T; ]0 T
值 | 长度 | 格式 |
0xffffffff | 9 | 0xff + uint64 |
varstr:变长字符串,由一个变长整数后接字符串构成。字符串采用 UTF8 编码。% e7 H6 f u3 b1 m* j; K
尺寸 | 字段 | 数据类型 | 说明 |
? | length | varint | 字符串的长度,以字节为单位 |
length | string | uint8[length] | 字符串本身 |
array:数组,由一个变长整数后接元素序列构成。
定点数
NEO 系统中的金额、价格等数据,统一采用 64 位定点数,小数部分精确到 10-8,可表示的范围是:[-263/108, +263/108)
数据结构2 D9 E Z* [$ X; ^! i! _: r% V
区块链
区块链是一种逻辑结构,它以单向链表的形式将区块串联起来,用于存放全网的交易、资产等数据。* m4 X J) l3 ^/ b1 N2 _$ n1 C
区块
尺寸 | 字段 | 数据类型 | 说明 |
4 | Version | uint32 | 区块版本,目前为 0 |
32 | PrevBlock | uint256 | 前一个区块的散列值 |
32 | MerkleRoot | uint256 | 交易列表的根散列 |
4 | Timestamp | uint32 | 时间戳 |
4 | Index | uint32 | 区块高度(区块索引) = 区块数量 - 1 |
8 | ConsensusData | uint64 | 共识数据(共识节点生成的伪随机数) |
20 | NextConsensus | uint160 | 下一个区块的记账合约的散列值 |
1 | - | uint8 | 固定为 1 |
? | Script | script | 用于验证该区块的脚本 |
?*? | Transactions | tx[] | 交易列表 |
在计算区块散列时,并不会把整个区块都计算在内,而是只计算区块头的前 7 个字段:Version, PrevBlock, MerkleRoot, Timestamp, Height, Nonce, NextMiner。由于 MerkleRoot 已经包含了所有交易的散列值,因此修改交易也会改变区块的散列值。
区块头的数据结构如下:- K' G* j7 G0 s
尺寸 | 字段 | 数据类型 | 说明 |
4 | Version | uint32 | 区块版本,目前为 0 |
32 | PrevBlock | uint256 | 前一个区块的散列值 |
32 | MerkleRoot | uint256 | 交易列表的根散列 |
4 | Timestamp | uint32 | 时间戳 |
4 | Index | uint32 | 区块高度(区块索引) = 区块数量 - 1 |
8 | ConsensusData | uint64 | 共识数据(共识节点生成的伪随机数) |
20 | NextConsensus | uint160 | 下一个区块的记账合约的散列值 |
1 | - | uint8 | 固定为 1 |
? | Script | script | 用于验证该区块的脚本 |
1 | - | uint8 | 固定为 0 |
每个区块的时间戳必须晚于前一个区块的时间戳,一般两个区块的时间戳相差 15 秒左右,但是也允许出现不精确的情况。区块的高度值必须恰好等于前一个区块的高度值加一。7 @/ U+ W9 P+ A, o& v( a8 @
交易
尺寸 | 字段 | 数据类型 | 说明 |
1 | Type | uint8 | 交易类型 |
1 | Version | uint8 | 交易版本,目前为 0 |
? | - | - | 特定于交易类型的数据 |
?*? | Attributes | tx_attr[] | 该交易所具备的额外特性 |
34*? | Inputs | tx_in[] | 输入 |
60*? | Outputs | tx_out[] | 输出 |
?*? | Scripts | script[] | 用于验证该交易的脚本列表 |
NEO 系统中的一切事务都以交易为单位进行记录。交易有以下几种类型:/ o+ \9 z' U7 D7 V" b
值 | 名称 | 系统费用 | 说明 |
0x00 | MinerTransaction | 0 | 用于分配字节费的交易 |
0x01 | IssueTransaction | 500|0 | 用于分发资产的交易 |
0x02 | ClaimTransaction | 0 | 用于分配 NeoGas 的交易 |
0x20 | EnrollmentTransaction | 1000 | (已弃用) 用于报名成为共识候选人的特殊交易 |
0x40 | RegisterTransaction | 10000|0 | (已弃用) 用于资产登记的交易 |
0x80 | ContractTransaction | 0 | 合约交易,这是最常用的一种交易 |
0xd0 | PublishTransaction | 500*n | (已弃用)智能合约发布的特殊交易 |
0xd1 | InvocationTransaction | 0 | 调用智能合约的特殊交易 |
每一种类型的交易除了具有交易的公共字段之外,还会具有自己的专属字段。关于不同类型交易的专属字段,下文会有详细说明。
MinerTransaction0 r! U: ~. V( h8 A/ @3 G
尺寸 | 字段 | 数据类型 | 说明 |
- | - | - | 交易的公共字段 |
4 | Nonce | uint32 | 随机数 |
- | - | - | 交易的公共字段 |
每一个区块的第一笔交易必然是 MinerTransaction。它用于将当前区块中所有的交易手续费奖励给记账人。
交易中的随机数用于防止出现散列冲突。. [" ?5 J9 a) n+ B' u; t+ i
IssueTransaction
资产发行交易没有额外的特殊字段。
资产管理员可以通过资产发行交易,将已经登记过的资产在 NEO 区块链上制造出来,并发送到任意地址。
特别的,如果发行的资产是 NEO,那么这笔交易将可以免费发送。
ClaimTransaction
尺寸 | 字段 | 数据类型 | 说明 |
- | - | - | 交易的公共字段 |
34*? | Claims | tx_in[] | 用于分配的 NEO |
- | - | - | 交易的公共字段 |
EnrollmentTransaction
4 u+ E( d- @, k, }
[!Warning]
已弃用,已被智能合约的 Neo.Blockchain.RegisterValidator 所替代。
查看 替代的 .NET 智能合约框架
查看 替代智能合约 API 2 ]# k% i% N& r( ^" o- O0 i9 a
RegisterTransaction
[!Warning]. ~) \9 ^6 _# O p, W" u
已弃用,已被智能合约的 Neo.Blockchain.CreateAsset 所替代。! j5 d, a9 d: ` m8 r0 x' E
( ]# V8 q; }7 | _5 T
查看 替代的 .NET 智能合约框架
查看 替代智能合约 API 6 l1 u: y- `- B! D
ContractTransaction. Q ^+ M% q- M& c: L" e8 J
合约交易没有任何特殊的地方。& P" T9 U3 }0 Q; N+ @
PublishTransaction
' [: e5 ^/ r4 x
[!Warning]5 h9 [ |1 J( z
已弃用,已被智能合约的 Neo.Blockchain.CreateContract 所替代。6 K8 T4 N2 m/ q; ]* u
' r5 ^/ h! f# `, n
查看 替代的 .NET 智能合约框架
查看 替代智能合约 API
InvocationTransaction
尺寸 | 字段 | 数据类型 | 说明 |
- | - | - | 交易的公共字段 |
? | Script | uint8[] | 所调用的智能合约的脚本 |
8 | Gas | int64 | 运行所调用的智能合约需要的费用 |
- | - | - | 交易的公共字段 |
交易特性( _% _% ]; h, n3 ~
尺寸 | 字段 | 数据类型 | 说明 |
1 | Usage | uint8 | 用途 |
0|1 | length | uint8 | 数据长度(特定情况下会省略) |
length | Data | uint8[length] | 特定于用途的外部数据 |
有时候交易中会需要包含一些供外部使用的数据,这些数据将统一被放置在交易特性字段中。
每个交易特性可以有不同的用途:1 L+ [ r& J# ^: G3 P! n/ O+ s
值 | 名称 | 说明 |
0x00 | ContractHash | 外部合同的散列值 |
0x02-0x03 | ECDH02-ECDH03 | 用于 ECDH 密钥交换的公钥 |
0x20 | Script | 用于对交易进行额外的验证 |
0x30 | Vote | 用于投票选出记账人 |
0x81 | DescriptionUrl | 外部介绍信息地址 |
0x90 | Description | 简短的介绍信息 |
0xa1-0xaf | Hash1-Hash15 | 用于存放自定义的散列值 |
0xf0-0xff | Remark-Remark15 | 备注 |
对于 ContractHash,ECDH 系列,Vote,Hash 系列,数据长度固定为 32 字节,length 字段省略;
对于 Script,数据长度固定为 20 字节,存放地址;
对于 DescriptionUrl,必须明确给出数据长度,且长度不能超过 255 字节;
对于 Description 和 Remark 系列,必须明确给出数据长度, 且长度不能超过 65535 字节。3 }9 {4 q$ t* k7 M1 K* i: Y5 O
交易输入% w. X4 G2 S; I/ H0 k2 v
尺寸 | 字段 | 数据类型 | 说明 |
32 | PrevHash | uint256 | 引用交易的散列值 |
2 | PrevIndex | uint16 | 引用交易输出的索引 |
交易输出
尺寸 | 字段 | 数据类型 | 说明 |
32 | AssetId | uint256 | 资产编号 |
8 | Value | int64 | 金额 |
20 | ScriptHash | uint160 | 收款地址 |
每个交易中最多只能包含 65536 个输出。
验证脚本2 e; [ M' h: X
尺寸 | 字段 | 数据类型 | 说明 |
? | StackScript | uint8[] | 栈脚本代码 |
? | RedeemScript | uint8[] | 合约脚本代码 |
栈脚本中只能包含压栈操作指令,用于向合约脚本传递参数(如签名等)。脚本解释器会先执行栈脚本代码,然后执行合约脚本代码。
在一笔交易中,合约脚本代码的散列值必须与交易输出中的一致,这是验证的一部分。关于脚本执行的过程,后文会详细阐述。7 k* k7 Y( r" i9 |, r7 b* V" }
网络消息/ W8 ]1 Q4 H2 O9 P
所有的网络消息都通过以下消息结构来发送:+ i' [; A3 {5 M6 P3 w
尺寸 | 字段 | 数据类型 | 说明 |
4 | Magic | uint32 | 协议标识号 |
12 | Command | char[12] | 命令 |
4 | length | uint32 | Payload 的长度 |
4 | Checksum | uint32 | 校验和 |
length | Payload | uint8[length] | 消息内容 |
已定义的 Magic 值:
值 | 说明 |
0x00746e41 | 正式网 |
0x74746e41 | 测试网 |
Command 采用 utf8 编码,长度为 12 字节,多余部分用 0 填充。- s8 e4 P- s: ^1 C1 v$ n9 }
Checksum 是 Payload 两次 SHA256 散列后的前 4 个字节。4 r8 ~! C f) y, ~( u
Payload 根据不同的命令有不同的详细格式,见下文。
version
尺寸 | 字段 | 数据类型 | 说明 |
4 | Version | uint32 | 协议版本,目前为 0 |
8 | Services | uint64 | 节点提供的服务,目前为 1 |
4 | Timestamp | uint32 | 当前时间 |
2 | Port | uint16 | 监听的端口,如果不监听则为 0 |
4 | Nonce | uint32 | 用于区分相同公网 IP 的节点 |
? | UserAgent | varstr | 客户端标识 |
4 | StartHeight | uint32 | 区块链高度 |
1 | Relay | bool | 是否接收并转发 |
一个节点收到连接请求时,它立即宣告其版本。在通信双方都得到对方版本之前,不会有其他通信。
verack
节点收到 version 消息后,立刻回复一个 verack 作为应答。' E; q H) P% p9 j' n( R; B
此消息没有 payload。5 u, I& C4 I8 q( m
getaddr
向一个节点请求一批新的活动节点,以增加自身的连接数。
此消息没有 payload。& Q3 G' [1 ]; z# W& d
addr
尺寸 | 字段 | 数据类型 | 说明 |
30*? | AddressList | net_addr[] | 网络上其他节点的地址 |
节点收到 getaddr 消息后,返回一个 addr 消息作为应答,提供网络上已知节点的信息。6 T3 j/ [5 ~5 i! g$ X4 B8 B% U# Q
getheaders
尺寸 | 字段 | 数据类型 | 说明 |
32*? | HashStart | uint256[] | 节点已知的最新 block 散列 |
32 | HashStop | uint256 | 请求的最后一个 block 的散列 |
向一个节点请求包含编号 HashStart 到 HashStop 的至多 2000 个 block 的 header 包。要获取之后的 block 散列,需要重新发送 getheaders 消息。这个消息用于快速下载不包含相关交易的 blockchain。
headers. c, N) I9 n! z
尺寸 | 字段 | 数据类型 | 说明 |
?*? | Headers | header[] | 区块头 |
节点收到 getheaders 消息后,返回一个 headers 消息作为应答,提供请求的区块头。
getblocks) F( b( \% \+ L4 d% i' }5 G
尺寸 | 字段 | 数据类型 | 说明 |
32*? | HashStart | uint256[] | 节点已知的最新 block 散列 |
32 | HashStop | uint256 | 请求的最后一个 block 的散列 |
向一个节点请求包含编号从 HashStart 到 HashStop 的 block 列表的 inv 消息。若 HashStart 到 HashStop 的 block 数超过 500,则在 500 处截止。欲获取后面的 block 散列,需要重新发送 getblocks 消息。
inv i- Y" |# ~; Z. Z. |
尺寸 | 字段 | 数据类型 | 说明 |
1 | Type | uint8 | 清单类型 |
32*? | Hashes | uint256[] | 清单 |
节点通过此消息可以广播它拥有的对象信息。这个消息可以主动发送,也可以用于应答 getbloks 消息。
清单类型有以下几种:0 ?/ o+ Q/ F! I8 l8 O( X
值 | 名称 | 说明 |
0x01 | TX | 交易 |
0x02 | Block | 区块 |
0xe0 | Consensus | 共识数据 |
getdata
尺寸 | 字段 | 数据类型 | 说明 |
1 | Type | uint8 | 清单类型 |
32*? | Hashes | uint256[] | 清单 |
向一个节点请求指定的对象,它通常在接收到 inv 包并滤去已知元素后发送。
block) Y5 M+ t- }" v% c" k( d
尺寸 | 字段 | 数据类型 | 说明 |
? | Block | block | 区块 |
向一个节点发送一个区块,用于响应请求数据的 getdata 消息。' a! j: f1 L5 o
tx! x l/ U; I3 i2 O g
尺寸 | 字段 | 数据类型 | 说明 |
? | Transaction | tx | 交易 |
向一个节点发送一笔交易,用于响应请求数据的 getdata 消息。