数据结构
在任意给定时刻, 一个节点总是连接到多个其他节点. 默认情况下, 一个节点连接到 8 个其他节点(链出), 并允许多达 125 个链入节点连接进来.
相关常量定义在 net.h 文件中.
节点集合则由全局变量 vNodes 维护.
CNode 用于表示一个节点.
CNode 包含许多属性, 其中大部分属性都与底层链路 (如套接字, 字节流等) 有关.
CNode 的关键属性如下:
nServices: 通常被称为 "服务位". 这是一个 bitmap 数据结构, 用于表示 peer 提供的服务种类. 具体种类如下.
NODE_NONE: 初值
NODE_NETWORK: 全节点. SPV 节点和其他类型的轻节点不设置该标识.
NODE_GETUTXO: 该节点能够响应 getutxo 协议请求. Bitcoin Core 节点不支持此功能. 此功能由一个由 Bitcoin XT 支持. BIP64 https://github.com/bitcoin/bips/blob/master/bip-0064.mediawiki
NODE_BLOOM:
NODE_WITNESS: 表示可以向节点询问块和交易, 包括见证数据. 应该是只支持隔离见证.
NODE_XTHIN: 表示节点支持 Xtreme Thinblocks
NODE_NETWORK_LIMITED: 表示节点仅支持最近的 288 年区块(即两天的数据)BIP159 https://github.com/bitcoin/bips/blob/master/bip-0159.mediawiki
fClient: 表示节点是否是 SPV 节点
fWhiteListed: 是否是白名单节点. 如果是的话, 该节点不会因为坏的行为被屏避掉.
vSendMsg: 队列中待发送的消息队列
vRecvMsg: 我们从 Peer 接收到的消息队列
节点发现和节点连接
地址管理
地址管理器用于管理节点的 IP 地址和端口.(见 addrman.h)
设计目标:
将地址表保存在内存中, 并将整个表异步转储到 peers.dat.
确保没有 (本地化的) 攻击者可以用它的节点 / 地址填充整个表格.
为此:
地址以 Bucket 的方式组织, 即地址被放到桶里.
尚未尝试过的地址进入 1024 个 "new" 桶.
已知可访问的节点地址进入 256 个 "tried" 桶.
每个地址范围随机选择 8 个桶
根据完整地址从实际存储桶中选择实际存储桶.
当为一个完整的桶添加一个新的好地址时, 一个随机选择的 entry(偏向于最近尝试过的)会被弹出, 回到 "新" 桶.
分组选择基于密码散列, 使用随机生成的 256 位密钥, 这不应该被攻击者观察到.
多个索引用于提升性能.
** 时间戳 **
地址管理器也要持续跟踪每个节点的最近活跃情况. 时间戳仅在一个地址上更新, 并且在时间戳超过 20 分钟时保存到数据库. 通过理解时间戳的作用, 将更加清楚的理解为什么要保持时间戳.
节点发现
节点发现从链路层来讲, 就是发现一个节点的 IP 地址和端口号. 有多种方式:
地址数据库, 即 peers.dat 文件
在节点启动时读入并加载到节点管理器中. 该方法在节点首次运行时是不支持的. 因为 peers.dat 文件还不存在.
用户指定(--addnode, --connect)
DNS 种子(DNS seeding)
仅当 peers.dat 文件为空时, DNS 种子才会被使用.
默认 DNS 种子有 6 个: seed.bitcoin.sipa.be, dnsseed.bluematt.me, dnsseed.bitcoin.dashjr.org, seed.bitcoinstats.com, seed.bitcoin.jonasschnelli.ch, seed.btc.petertodd.org
硬编码的种子
如果 DNS seeding 失败, 客户端使用硬编码的种子.[chainparamsseeds.h] 这些地址仅用作最后的手段. 理想情况是尽快远离种子节点, 以避免过载这些节点. DNS 种子和硬编码的种子的时间戳均为 0, 这样可以避免此类地址被广播到网络中或响应 getaddr.
从其他 Peer 获取(getaddr 和 addr 消息)
节点间通过 getaddr 和 addr 交换 IP 地址信息.
但是,"addr" 消息也可能会不请自来, 因为节点在以下情况时无偿地发布地址:
当节点转发地址时
周期性广播节点自己的地址(每 24 小时)
当连接建立时(响应 version 消息时带回)
节点发送 getaddr 消息的时机
在响应 version 消息, 并且节点自身的地址量小于 1000 时.
当接收到 addr 时:
发送消息的节点版本太老, 并且我们已经有 1000 个节点地址了, 该消息被忽略.
如果节点是当前版本, 并且尝试向我们发送超过 1000 个节点地址, 则该节点会被惩罚.
如果该地址 24 小时内已经发现, 并且当前时间戳超过 60 分种, 则时间戳更新为 60 分种.
如果该地址 24 小时内没有出现过, 而时间戳是 24 小时之前, 则更新为 24 小时.
当响应 getaddr 消息时:
该节点计算出在过去 3 个小时内有多少个地址有时间戳.
它发送这些地址, 但如果有超过 2500 个地址, 它随机选择 2500.
它清除我们认为远程节点所拥有的地址列表, 这将触发发送到节点的刷新. 请参阅 SendMessages.
地址中继:
一旦新地址被添加
地址的时间戳在 10 分钟之内
addr 消息包括 10 个地址或少一些
fGetAddr==false
该地址是可路由的
满足以上条件, 则新地址会被随机的发送出去
节点连接
节点链接由线程 ThreadOpenConnections 管理. 它负责选择可用的地址, 建立链接并在适当的时候释放链接.
而 inbound 链接则由 ThreadSocketHandler 负责处理[ThreadMessageHandler].
对于 inbound 链接, 系统通过 select 执行 IO 操作. 所以, 其支持的 inbound 数量不会太多, 目前, 是 125 个.
插口 (Sockets) 和消息
Socket 线程 (net.cpp)
消息线程
ProcessMessages (net_processing.cpp)
ProcessMessages 是 net_processing.cpp 中的处理和验证交易和区块等相关代码代码的入口点, 同时它也处理 getaddr, addr 等比特币协议相关的消息. 该方法主要完成消息的复制并调用 ProcessMessage 处理消息.
ProcessMessage 基本上是一个大型的 "开关", 它根据消息类型 [NetMsgType] 来采取行动.
消息类型列表如下[protocol.cpp]:
- namespace NetMsgType {
- const char *VERSION="version";
- const char *VERACK="verack";
- const char *ADDR="addr";
- const char *INV="inv";
- const char *GETDATA="getdata";
- const char *MERKLEBLOCK="merkleblock";
- const char *GETBLOCKS="getblocks";
- const char *GETHEADERS="getheaders";
- const char *TX="tx";
- const char *HEADERS="headers";
- const char *BLOCK="block";
- const char *GETADDR="getaddr";
- const char *MEMPOOL="mempool";
- const char *PING="ping";
- const char *PONG="pong";
- const char *NOTFOUND="notfound";
- const char *FILTERLOAD="filterload";
- const char *FILTERADD="filteradd";
- const char *FILTERCLEAR="filterclear";
- const char *REJECT="reject";
- const char *SENDHEADERS="sendheaders";
- const char *FEEFILTER="feefilter";
- const char *SENDCMPCT="sendcmpct";
- const char *CMPCTBLOCK="cmpctblock";
- const char *GETBLOCKTXN="getblocktxn";
- const char *BLOCKTXN="blocktxn";
- }
- SendMessages (main.cpp)
SendMessages 创建消息并在 peer 的 vSendMsg 队列 (双端队列或 C ++ 中的 "deque") 中对其进行排队. vSendMsg 对象基本上只是序列化的数据.
Locks
P2P 层主要的锁有:
cs_vNodes: 控制对 CNode 对象的访问
cs_vSend: 控制对节点的发送缓存的访问
cs_vRecvMsg: 控制对节点的接收缓存的访问
cs_inventory
拒绝服务的防范措施
一旦发现异常行为的节点, 则直接 ban 掉.
DoS 预防框架在 2011 年引入.
具体见: https://github.com/bitcoin/bitcoin/pull/517
** 被禁节点 **
被禁节点存储在 setBanned 中. setBanned 是 map 类型的数据结构, 定义为
- typedef std::map<CSubNet, CBanEntry> banmap_t
- .
默认情况下, 一个节点被禁止 24 小时, 但可以使用 - bantime 选项进行配置.
来源: http://www.jianshu.com/p/3e6aadef7673