【区块链】比特币学习 - 6 - 比特币网络

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/loy_184548/article/details/86178368

比特币学习 - 6 - 比特币网络

参考博客:here and here

一、节点

比特币采用了基于国际互联网(Internet)的P2P(peer-to-peer)网络架构。P2P是指位于同一网络中的每台计算机都彼此对等,各个节点共同提供网络服务,不存在任何“特殊”节点。每个网络节点以“扁平(flat)”的拓扑结构相互连通。在P2P网络中不存在任何服务端(server)、中央化的服务、以及层级结构。

1. 概念

尽管比特币P2P网络中的各个节点相互对等,但是根据所提供的功能不同,各节点可能具有不同的分工。

名称 应用
FULL NODE 钱包、矿工、完整区块链、网络路由节点
完整区块链节点 完整区块链、网络路由节点
独立矿工 矿工、完整区块链、网络路由节点
SPV NODE 钱包、网络路由节点
挖矿节点 矿工、POOL 服务器、Stratum 服务器

解释几种应用:

钱包:具备钱包功能的节点可以支持比特币交易,查询等功能。

矿工:具备矿工功能的节点可以通过解决工作量证明算法难题来争夺创建新块的资格从而获取新的比特币和收取交易手续费。

完整区块:具备完整区块的节点即存储着整条区块链完整数据,可以独立的验证所有交易而不需要外部参照。

路由网络:所有的基础比特币节点均具有路由的功能,具有路由网络的节点能帮助转发交易和区块数据,发现和维护节点间的连接。

2.数据结构

以下为节点定义:

// 比较复杂,截取一部分
class CNode
{
    friend class CConnman;
public:
    // socket
    std::atomic<ServiceFlags> nServices;
    SOCKET hSocket;		// 用来连接socket
    size_t nSendSize; 	// 所有vSendMsg条目的总大小。
    size_t nSendOffset; // 已经发送的第一个vSendMsg内的偏移量。
    uint64_t nSendBytes;
    std::deque<std::vector<unsigned char>> vSendMsg;	//	发送消息的数组
    ...
    const CAddress addr;		// 节点的地址信息
    const CAddress addrBind;	// Bind address of our side of the connection
    std::atomic<int> nVersion;	//版本信息
    ...
};

二、网络发现

当新的网络节点启动后,为了能够参与协同运作,它必须发现网络中的其他比特币节点。新的网络节点必须发现至少一个网络中存在的节点并建立连接。由于比特币网络的拓扑结构并不基于节点间的地理位置,因此各个节点之间的地理信息完全无关。在新节点连接时,可以随机选择网络中存在的比特币节点与之相连。

所有节点会定期发送信息以维持连接,90分钟无通信则认为是断开,网络将开始查找新的对等节点

代码目录:src/net.cpp

// 新节点开始
bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
{
    ...
    // 从peers.dat和banlist.dat加载已经保存的节点的地址
    CAddrDB adb;
    if (adb.Read(addrman)) { ... }
   	// 开始线程
    ... 
    // 1. net: 接收和发送sockets,并监听其他节点的连接请求
    threadSocketHandler = std::thread(&TraceThread<std::function<void()> >, "net", std::function<void()>(std::bind(&CConnman::ThreadSocketHandler, this)));
	// 2. dnsseed: 通过dns查询解析出种子节点的地址,之后新启动的节点将要向这些种子节点发起连接
    threadDNSAddressSeed = std::thread(&TraceThread<std::function<void()> >, "dnsseed", std::function<void()>(std::bind(&CConnman::ThreadDNSAddressSeed, this)));
	// 3. opencon: 负责向已发现的节点发起连接
    threadOpenConnections = std::thread(&TraceThread<std::function<void()> >, "opencon", std::function<void()>(std::bind(&CConnman::ThreadOpenConnections, this, connOptions.m_specified_outgoing)));
	// 4. msghand: 负责比特币P2P协议的消息处理
    threadMessageHandler = std::thread(&TraceThread<std::function<void()> >, "msghand", std::function<void()>(std::bind(&CConnman::ThreadMessageHandler, this)));
	...
}

接下来结合这几个线程来分析:

1. 种子节点

代码目录:src/chainparams.cpp

在这里内置了一些种子节点:

vSeeds.emplace_back("seed.bitcoin.sipa.be"); // Pieter Wuille, only supports x1, x5, x9, and xd
vSeeds.emplace_back("dnsseed.bluematt.me"); // Matt Corallo, only supports x9
vSeeds.emplace_back("dnsseed.bitcoin.dashjr.org"); // Luke Dashjr
vSeeds.emplace_back("seed.bitcoinstats.com"); // Christian Decker, supports x1 - xf
vSeeds.emplace_back("seed.bitcoin.jonasschnelli.ch"); // Jonas Schnelli, only supports x1, x5, x9, and xd
vSeeds.emplace_back("seed.btc.petertodd.org"); // Peter Todd, only supports x1, x5, x9, and xd
vSeeds.emplace_back("seed.bitcoin.sprovoost.nl"); // Sjors Provoost

通过dnsseed进行处理种子节点:

void CConnman::ThreadDNSAddressSeed()
{
    ...
	// 取出内置种子节点
    const std::vector<std::string> &vSeeds = Params().DNSSeeds();
    ...
    // 遍历种子
    for (const std::string &seed : vSeeds) {
        ...
        // 进行DNS查询
		if (LookupHost(host.c_str(), vIPs, nMaxIPs, true))
        {
            for (const CNetAddr& ip : vIPs)
            {
                int nOneDay = 24*3600;
                CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits);
                addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); 
                vAdd.push_back(addr);
                found++;
            }
            // 解析到的ip地址将存入CAddrMan
            addrman.Add(vAdd, resolveSource);
        } 
        ...
    }
}

2. 发起连接并初始化节点

新的网络节点必须发现至少一个网络中存在的节点并建立连接。

通过opencon线程进行处理:

void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound, const char *pszDest, bool fOneShot, bool fFeeler, bool manual_connection)
{
   	...
    // 创建节点
    CNode* pnode = ConnectNode(addrConnect, pszDest, fCountFailure, manual_connection);
	...
    // 初始化节点
    m_msgproc->InitializeNode(pnode);
    {
        LOCK(cs_vNodes);
        vNodes.push_back(pnode);
    }
}
A. 发起连接
CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, bool manual_connection)
{
    /* 第一步:先做一些检查 */
    // 查找现有连接
    CNode* pnode = FindNode(static_cast<CService>(addrConnect));
    // 如果已经有连接,则不继续新连接
    if (pnode) { ... }
	...
       
    /* 第二步:进行连接 */
    bool connected = false;
    SOCKET hSocket = INVALID_SOCKET;
    proxyType proxy;
    if (addrConnect.IsValid()) {
        bool proxyConnectionFailed = false;
		// 对是否设置代理分别处理
        if (GetProxy(addrConnect.GetNetwork(), proxy)) {
            hSocket = CreateSocket(proxy.proxy);
            if (hSocket == INVALID_SOCKET) {
                return nullptr;
            }
            connected = ConnectThroughProxy(proxy, addrConnect.ToStringIP(), addrConnect.GetPort(), hSocket, nConnectTimeout, &proxyConnectionFailed);
        } else {
            // 不设置代理,直接通过ConnectSocketDirectly来连接,其内部是调用socket api的connect函数
            hSocket = CreateSocket(addrConnect);
            if (hSocket == INVALID_SOCKET) {
                return nullptr;
            }
            connected = ConnectSocketDirectly(addrConnect, hSocket, nConnectTimeout, manual_connection);
        }
        // 到这里为止,两个节点的连接已经建立,可以互相通数据
        ...
            
    /* 第三步:创建新节点 */   
    ...
    CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", false);
    pnode->AddRef();
    ...
}
B. 初始化节点

版本消息始终是任何节点发送给另一个节点的第一条消息。接收版本消息的本地对等体将检查远程对等体报告的nVersion,并确定远端对等体是否兼容。 如果远程对等体兼容,则本地对等体将确认版本消息,并通过发送一个verack建立连接。

version信息:

PROTOCOL_VERSION: 比特币P2P协议版本
nLocalServices:该节点支持的本地服务列表
nTime:当前时间
addrYou:当前节点可见的远程节点IP
addrME:本地节点所发现的本机IP
subver:当前节点运行的软件类型
BaseHeight:当前节点区块链的区块高度

来看一下代码部分:

void PeerLogicValidation::InitializeNode(CNode *pnode) {
    CAddress addr = pnode->addr;
    std::string addrName = pnode->GetAddrName();
    NodeId nodeid = pnode->GetId();
    {
        LOCK(cs_main);
        mapNodeState.emplace_hint(mapNodeState.end(), std::piecewise_construct, std::forward_as_tuple(nodeid), std::forward_as_tuple(addr, std::move(addrName)));
    }
    if(!pnode->fInbound)
        PushNodeVersion(pnode, connman, GetTime());
}

2. 地址传播

当建立一个或多个连接后,新节点将一条包含自身IP地址的addr消息发送给其相邻节点。相邻节点再将此条addr消息依 次转发给它们各自的相邻节点,从而保证新节点信息被多个节点所接收、保证连接更稳定。另外,新接入的节点可以向 它的相邻节点发送getaddr消息,要求它们返回其已知对等节点的IP地址列表。通过这种方式,节点可以找到需连接到 的对等节点,并向网络发布它的消息以便其他节点查找。

整理一下,新节点需要做两件事情:

  1. 将新节点addr扩散出去
  2. 新节点请求获取其他节点地址

首先看一下收集本机地址的代码部分:

// 获取所有本地网络接口地址,并保存起来
void Discover()
{
	...
    struct ifaddrs* myaddrs;
    // 获取所有本地网络接口地址
    if (getifaddrs(&myaddrs) == 0)
    {
        for (struct ifaddrs* ifa = myaddrs; ifa != nullptr; ifa = ifa->ifa_next)
        {
            if (ifa->ifa_addr == nullptr) continue;
            if ((ifa->ifa_flags & IFF_UP) == 0) continue;
            if (strcmp(ifa->ifa_name, "lo") == 0) continue;
            if (strcmp(ifa->ifa_name, "lo0") == 0) continue;
            if (ifa->ifa_addr->sa_family == AF_INET)
            {
                struct sockaddr_in* s4 = (struct sockaddr_in*)(ifa->ifa_addr);
                CNetAddr addr(s4->sin_addr);
                // 将这些地址添加到保存本地地址的全局变量中
                if (AddLocal(addr, LOCAL_IF))
                    LogPrintf("%s: IPv4 %s: %s\n", __func__, ifa->ifa_name, addr.ToString());
            }
            else if (ifa->ifa_addr->sa_family == AF_INET6)
            {
                struct sockaddr_in6* s6 = (struct sockaddr_in6*)(ifa->ifa_addr);
                CNetAddr addr(s6->sin6_addr);
                if (AddLocal(addr, LOCAL_IF))
                    LogPrintf("%s: IPv6 %s: %s\n", __func__, ifa->ifa_name, addr.ToString());
            }
        }
        freeifaddrs(myaddrs);
    }
}

地址被预先收集完成,接下来这些地址将被广播出去,消息的发送和接收将在下文进行介绍。

三、区块同步

1. FULL NODE

一个全节点连接到对等节点之后,第一件要做的事情就是构建完整的区块链。如果该节点是一个全新节点,那么它就不包含任何区块链信息,它只知道一个区块——静态植入在客户端软件中的创世区块。新节点需要下载从0号区块(创世区块)开始的数十万区块的全部内容,才能跟网络同步、并重建全区块链。

第一步:节点A与节点B互相发送信息,调用getblocks,报刊本地区块链顶端哈希以及最高块高度

第二步:(此处假设节点B更长)节点B可识别出哪些区块是节点A需要补充的,使用INV消息将这些区块哈希传播出去

第三步:节点A向所有与之相连的对等节点发送getdata请求数据(目的是为了分摊工作量)

全节点交易验证方式

全节点沿着区块链按时间倒叙一直追溯到创世区块,建立一个完整的UTXO数据库,通过查询UTXO是否未被支付来验证交易的有效性

2. SPV NODE

SPV节点只需下载区块头,而不用下载包含在每个区块中的交易信息,大小约只有全节点的1/1000。

第一步:SPV节点使用的是一条getheaders消息来获得区块头

第二步:发出相应的对等节点将用一条headers消息发送多达2000个区块头

SPV节点交易验证方式

SPV节点通过向其他节点请求某笔交易的Merkle路径,如果路径正确无误,并且该交易之上已有6个或以上区块被确认,则证明该交易不是双重支付。

猜你喜欢

转载自blog.csdn.net/loy_184548/article/details/86178368