☰
四十三.创建一笔交易(6)--第二个区块BitcoinMiner
2026/10/30 2:31:47 网站建设 项目流程

现在我们来修复BitcoinMiner里的代码

1.IsFinal

这个函数未定义,这个函数是属于CTransaction类,我们在main.h相类里定义一下就行,代码如下:

bool IsFinal() const { if (nLockTime == 0 || nLockTime < nBestHeight) return true; foreach(const CTxIn& txin, vin) if (!txin.IsFinal()) return false; return true; }

这个函数,大概就是可以设定一笔交易,满足什么时间或区块高才能上链。也就是说我可以规定这笔交易目前不上链,等达到我的要求才能被区块接收。具体是什么样,需要看代码设计。

这里我们只要修复就行,因为现在默认都是即时上链。可以无视,返回的都是true。

将此代码复制进去,然后将foreach循环改成for的写法。

2.CTxIn IsFinal

然后下级IsFinal又报未定义,我们去maih.h里,在CTxIn类里添加如下代码就行:

bool IsFinal() const { return (nSequence == UINT_MAX); }

好,IsFinal函数未定义问题已解决。

3.GetMinFee

接下来看CTransaction类的第二个函数,GetMinFee未定义,这个函数之前提到过。

就是获取这笔tx所需要的手续费(根据系统设定)。

我们去main.h里定义:

int64 GetMinFee(bool fDiscount=false) const { unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK); if (fDiscount && nBytes < 10000) return 0; return (1 + (int64)nBytes / 1000) * CENT; }

然后CENT未定义,同样在maih.h里定义:

static const int64 CENT = 1000000;

好,GetMinFee问题已解决。

4.GetBlockValue

这个函数属于CBlock,我们需要在main.h里的CBlock类里声明一下:

int64 GetBlockValue(int64 nFees) const;

然后再到main.cpp里相关处添加定义:

即这部分后面:

////////////////////////////////////////////////////////////////////////////// // // CBlock and CBlockIndex //
int64 CBlock::GetBlockValue(int64 nFees) const { int64 nSubsidy = 50 * COIN; // Subsidy is cut in half every 4 years nSubsidy >>= (nBestHeight / 210000); return nSubsidy + nFees; }
5.ProcessBlock

好,现在还有最后一个函数报错--ProcessBlock,未定义。

但这个函数涉及的功能比较多,之前我们也看过调用链,就单向,调用了非常多的函数,还没有算上分支上的函数。

所以我们具体看一下,如果需要修复的下级函数过多,就是跟目前不相关的功能太多,我们需要注释掉。或者将需要的功能单独提出来写一个函数。

剩下的等正常交易时解决一部分代码,然后恢复网络功能时解决另一部分问题。

这个函数暂时不急着修复,我们先来看一下。

由于我们挖到矿后,就是第二个区块生成,我们需要区块上链,写入数据库,构建索引,维护各种表,以及最优链设置等,都得通过ProcessBlock函数实现,所以这个函数不能直接注释掉。

我们来看一下这个函数据具体代码:

// 处理接收到的区块 // pfrom:发送该区块的节点;如果区块不是通过网络节点接收的,可以为 NULL // pblock:待处理的区块指针 // 返回值:true 表示该区块被正常处理,或被成功暂存为孤块; // false 表示处理过程中出现错误 bool ProcessBlock(CNode* pfrom, CBlock* pblock) { // ==================== 1. 检查区块是否重复 ==================== // 计算当前区块的哈希值,作为区块的唯一标识 uint256 hash = pblock->GetHash(); // 检查当前区块是否已经存在于主区块索引表中 // mapBlockIndex 保存已经建立索引的区块信息 if (mapBlockIndex.count(hash)) // 如果已经存在,说明该区块之前已经处理过 // 输出错误信息:区块高度、区块哈希前14个字符 // 并返回 false return error("ProcessBlock() : already have block %d %s", mapBlockIndex[hash]->nHeight, hash.ToString().substr(0,14).c_str()); // 检查当前区块是否已经存在于孤块集合中 // mapOrphanBlocks 保存缺少前序区块、暂时无法接入的区块 if (mapOrphanBlocks.count(hash)) // 如果该区块已经作为孤块保存,就不再重复处理 // 输出错误信息并返回 false return error("ProcessBlock() : already have block (orphan) %s", hash.ToString().substr(0,14).c_str()); // ==================== 2. 进行初步合法性检查 ==================== // 调用区块自身的检查函数,检查区块的基本合法性 // 例如:区块大小、交易数量、交易结构、Merkle Root、 // 工作量证明(PoW)等具体规则,取决于该版本的 CheckBlock() 实现 if (!pblock->CheckBlock()) { // 区块未通过初步检查,释放动态分配的区块对象 delete pblock; // 返回错误,表示区块检查失败 return error("ProcessBlock() : CheckBlock FAILED"); } // ==================== 3. 检查前一个区块是否存在 ==================== // 检查当前区块所引用的前一个区块是否已存在于区块索引表 // hashPrevBlock 表示当前区块的前序区块哈希 if (!mapBlockIndex.count(pblock->hashPrevBlock)) { // 找不到前序区块,说明当前区块暂时无法连接到已知区块链 // 因此将它视为孤块(Orphan Block) // 打印其前序区块哈希的前14个字符,方便调试 printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,14).c_str()); // 将当前区块保存到孤块映射表中 // key 是当前区块哈希,value 是区块对象指针 // 这样后续可以根据区块哈希查找这个孤块 mapOrphanBlocks.insert(make_pair(hash, pblock)); // 同时按前序区块哈希建立索引 // key 是缺失的前序区块哈希,value 是孤块指针 // 这样当缺失的前序区块到达时,可以快速找到依赖它的孤块 mapOrphanBlocksByPrev.insert( make_pair(pblock->hashPrevBlock, pblock) ); // 如果该区块来自网络节点,就向发送方请求相关区块数据 if (pfrom) // 发送 getblocks 消息,请求从当前最佳区块链的位置开始, // 获取到孤块根节点附近所需的区块 // CBlockLocator(pindexBest):根据当前最佳链构造区块定位器 // GetOrphanRoot(pblock):找到该孤块链上最早的孤块 // 目的是补齐缺失的前序区块 pfrom->PushMessage( "getblocks", CBlockLocator(pindexBest), GetOrphanRoot(pblock) ); // 孤块已经成功暂存,等待依赖的区块到达 // 这里返回 true 并不代表它已经正式接入区块链 return true; } // ==================== 4. 接受并保存当前区块 ==================== // 前序区块已存在,尝试正式接受当前区块 // AcceptBlock() 通常还会进行上下文相关的验证, // 并在验证通过后将区块写入磁盘、建立区块索引等 if (!pblock->AcceptBlock()) { // 接受失败,释放当前区块对象 delete pblock; // 返回错误,表示区块未能通过正式接受流程 return error("ProcessBlock() : AcceptBlock FAILED"); } // 当前区块已被接受,释放本函数持有的区块对象 // 注意:这里假设 AcceptBlock() 已经完成必要的数据保存, // 不再需要调用者继续持有这个 pblock 对象 delete pblock; // ==================== 5. 查找并处理依赖当前区块的孤块 ==================== // 创建工作队列,用于保存已经接受、需要继续向后处理的区块哈希 // 通过工作队列,可以逐步处理依赖链上的孤块 vector<uint256> vWorkQueue; // 将刚刚接受的区块哈希放入队列 // 以它为起点,寻找那些以前因缺少它而被暂存的孤块 vWorkQueue.push_back(hash); // 遍历工作队列 // 当处理某个区块后,如果又成功接受了新的孤块, // 就把新孤块的哈希加入队列,继续处理下一层依赖 for (int i = 0; i < vWorkQueue.size(); i++) { // 取出当前正在处理的区块哈希 // 它可能是最初接受的区块,也可能是后来接受的孤块 uint256 hashPrev = vWorkQueue[i]; // 在 mapOrphanBlocksByPrev 中查找: // 所有以前序区块哈希 hashPrev 为键的孤块 // lower_bound(hashPrev) 指向该键对应范围的起点 // upper_bound(hashPrev) 指向该键对应范围的终点 // 因为这是 multimap,所以同一个前序区块可以对应多个孤块 for (multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); mi != mapOrphanBlocksByPrev.upper_bound(hashPrev); ++mi) { // 取得依赖当前前序区块的孤块对象 CBlock* pblockOrphan = (*mi).second; // 尝试正式接受这个孤块 // 此时它的直接前序区块已经被接受, // 因此可以继续进行上下文验证和区块接入流程 if (pblockOrphan->AcceptBlock()) // 如果接受成功,将该孤块的哈希加入工作队列 // 后续继续查找以这个新区块为前序区块的其他孤块 vWorkQueue.push_back(pblockOrphan->GetHash()); // 无论 AcceptBlock() 是否成功,都将该孤块 // 从按区块哈希索引的孤块表中移除 mapOrphanBlocks.erase(pblockOrphan->GetHash()); // 释放孤块对象占用的内存 // 注意:即使 AcceptBlock() 失败,这里也会删除该孤块, // 不会继续保留它等待重试 delete pblockOrphan; } // 当前前序区块对应的孤块已经全部处理完毕 // 删除 mapOrphanBlocksByPrev 中以 hashPrev 为键的所有记录 // 避免留下无用的孤块索引 mapOrphanBlocksByPrev.erase(hashPrev); } // ==================== 6. 输出成功信息并返回 ==================== // 打印日志,表示本次区块处理流程已经完成 printf("ProcessBlock: ACCEPTED\n"); // 返回 true,表示当前区块已被接受, // 并且本次发现的相关孤块也已尝试处理完毕 return true; }

这个函数一大部分涉及到孤块处理,它的处理逻辑有点多,我们看情况可以注释掉,因为我们是单节点,不存在从从他节点接收区块,也不存在什么网络延迟这种问题。

所以我们可以选择注释掉。

6.收集孤块

我们先把添加孤块的部分注释掉,就是在正常索引中找不到这个块的父块,就把它添加孤块数组中:

/* //If don't already have its previous block, shunt it off to holding area until we get it if (!mapBlockIndex.count(pblock->hashPrevBlock)) { printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0, 14).c_str()); mapOrphanBlocks.insert(make_pair(hash, pblock)); mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); Ask this guy to fill in what we're missing if (pfrom) pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), GetOrphanRoot(pblock)); return true; }*/

孤块集:mapOrphanBlocks

另一个mapOrphanBlocksByPrev,是关联这个孤块的父哈希。即给这个父哈希关联它的儿子。

即方便点在于,下次接到一个块后,可以直接查这个,看看这个块有没有儿子。

7.处理孤块

是怎么处理孤块的呢?比如现在来了一个区块,已经通过验证呢,那就寻找这个块有没有儿子,就是在孤块集里寻找。

//// Recursively process any orphan blocks that depended on this one //vector<uint256> vWorkQueue; //vWorkQueue.push_back(hash); //for (int i = 0; i < vWorkQueue.size(); i++) //{ // uint256 hashPrev = vWorkQueue[i]; // for (multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); // mi != mapOrphanBlocksByPrev.upper_bound(hashPrev); // ++mi) // { // CBlock* pblockOrphan = (*mi).second; // if (pblockOrphan->AcceptBlock()) // vWorkQueue.push_back(pblockOrphan->GetHash()); // mapOrphanBlocks.erase(pblockOrphan->GetHash()); // delete pblockOrphan; // } // mapOrphanBlocksByPrev.erase(hashPrev); //}

这部分可以注释掉,可以看到,先把这个块的哈希加到vWorkQueue里。

但是后面为什么要用for循环遍历它呢,此时它只有一个元素啊。这是为了,如果你添加的儿子区块,如果他也有一个儿子孤块呢,所以这里就会把,比如说1,2,3区块,也会把2区块哈希添加到vWorkQueue里,所以继续寻找。

但是这里有个问题,2号区块,被添加到孤块时,他必定是通过验证了,后续他也会处理他的儿子区块,为什么在这里还要往下遍历2号区块的儿子块呢。

这是因为:比如说,2号区块最先到,然后才是3号区块,最后才是1号区块。

当然实际是,如果2号区块先到,那么它没有父区块,此时只能被做为孤块存储。

那么3号区块到了,他同样在mapBlockIndex里找不到2号区块。

所以处理逻辑是,不管有多少孤块,必须是先处理先写入链上的那个区块的下一个区块。

然后才是儿子孙子区块,这样的按顺序的一个个处理写入。

8.多个儿子

我们来看一下,根据这个区块的哈希,然后从mapOrphanBlocksByPrev找它的儿子块,是怎么查找的。先来看for循环遍历:

for (multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); mi != mapOrphanBlocksByPrev.upper_bound(hashPrev); ++mi)

8.1lower_bound

这个函数,就是根据提供的参数,也就是hashPrev,找到第一个大于或等于该值的元素,返回迭代器(注意按排序的来)。

而upper_bound就是,它返回大于hashPrev的第一个元素的迭代器。

比如,1,2,5,5,5,6,7 找的值是5,那么lower_bound函数返回的是第三个5,而upper_bound大于5的第一个元素是6,即返回6那个迭代器。

其实我不太喜欢在for里面写很多的算法,表达式之类的,我认为for就是循环语句,就该干它原本的工作,这样便于阅读。

为了便于阅读,我们可以改成这样的写法:

// 第一步:找到父区块哈希对应的孤块范围起点 multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); // 第二步:找到这个范围的终点 multimap<uint256, CBlock*>::iterator miEnd = mapOrphanBlocksByPrev.upper_bound(hashPrev); // 第三步:遍历所有以 hashPrev 为父哈希的孤块 for (; mi != miEnd; ++mi) { // 取出当前孤块指针 CBlock* pblockOrphan = (*mi).second; // 后续处理逻辑…… }

可以看到这是范围查找,说明一个区块可能同时有多个儿子块,分叉了,我们都需要处理。

处理代码很简单,把这个儿子块写入一下数据库上链:

8.1添加儿子块

if (pblockOrphan->AcceptBlock())

如果成功了,则将它添加到vWorkQueue里,可以把它当成一个父块了,继续找它还有没有儿子孤块:

8.2找儿子块的后代

vWorkQueue.push_back(pblockOrphan->GetHash());

回到上一步:然后不管成不成功,都会从孤块中把这个儿子块删掉。为什么,因为你已经找到你的父块了,还添加不上,那已经没必要了,说明有问题:

8.3删除儿子块

mapOrphanBlocks.erase(pblockOrphan->GetHash()); delete pblockOrphan;

8.4处理mapOrphanBlocksByPrev

当此区块的关联的孤块全部处理完毕后,就可以从关联中删除掉了,后续将不在有此区块儿子孤块产生,因为此区块已经上链了。

mapOrphanBlocksByPrev.erase(hashPrev);

好,以上就是孤块的处理逻辑,注意这里有一些点需要说明,如果是有多个儿子块,那么acceptblock都接收了,最优链是在哪里选择的呢?

在acceptblock函数中,后面会调用AddToBlockIndex函数,这个函数会处理这些问题。相关情况,看该函数代码即可。

好,我们再来汇总一下这个孤块处理逻辑,首先这个块能被寻找儿子块,那必须得是自己不能是孤块,如果自己是孤块,在前面的代码已经被return true了。也就是说此时这个块,它的所有上级都是有的。

然后我们就在这个块里,从孤块集里,找出它能拼凑出的最长块。并且可以是多条。

比如现在这真块是第123个,它就会找,第124,再找125。如果有就会直接找下去。

包括分支,124-1,125-1。 也是一样。

这个逻辑确保了工作不会出错,不会有遗漏,不会说。

比如说现在是第125个区块,没有父块,我把它添加到孤块里,然后126区块来了,也没有父块(没写入数据库),接着124区块来了,然后找它的儿子块,找到是第125个。把125的区块加进去。但却不找125区块的儿子块(126上不了链)。然后是127区块来了,接着后面一直出错都是孤块了,第126的那个区块没有逻辑去处理它了。

好,写了这么多关于孤块说明,是即为了后续的学习做准备,又知道了该怎么注释代码。

我们可以把涉及到所有孤块的代码都注释掉,在这里是没问题的。

9.修复函数

那么此时后面ProcessBlock就剩下两个功能,分别是验证区块CheckBlock,和接收区块AcceptBlock。

把这两个函数修复好后,ProcessBlock也就是修复完成,我们就可以创建第二个真实区块了。

10.CheckBlock

这个函数,就是检测一下区块的基本规则是不是合法,比如区块大小,区块的时间,难度符不符合要求等。还有矿工是不伪造多笔coinbase。

我们先添加CheckBlock函数,在CBlock类中,main.cpp 和main.h分别添加定义和声明。

此函数代码如下:

bool CBlock::CheckBlock() const { // These are checks that are independent of context // that can be verified before saving an orphan block. // Size limits if (vtx.empty() || vtx.size() > MAX_SIZE || ::GetSerializeSize(*this, SER_DISK) > MAX_SIZE) return error("CheckBlock() : size limits failed"); // Check timestamp if (nTime > GetAdjustedTime() + 2 * 60 * 60) return error("CheckBlock() : block timestamp too far in the future"); // First transaction must be coinbase, the rest must not be if (vtx.empty() || !vtx[0].IsCoinBase()) return error("CheckBlock() : first tx is not coinbase"); for (int i = 1; i < vtx.size(); i++) if (vtx[i].IsCoinBase()) return error("CheckBlock() : more than one coinbase"); // Check transactions for(const CTransaction tx: vtx) if (!tx.CheckTransaction()) return error("CheckBlock() : CheckTransaction failed"); // Check proof of work matches claimed amount if (CBigNum().SetCompact(nBits) > bnProofOfWorkLimit) return error("CheckBlock() : nBits below minimum work"); if (GetHash() > CBigNum().SetCompact(nBits).getuint256()) return error("CheckBlock() : hash doesn't match nBits"); // Check merkleroot if (hashMerkleRoot != BuildMerkleTree()) return error("CheckBlock() : hashMerkleRoot mismatch"); return true; }

将foreach改成for的写法后,只有一个报错:

if (!tx.CheckTransaction())

这个函数未定义,这是更细致的一层检查,每笔tx检查一下。

我们在main.h里的类定义一下:

bool CheckTransaction() const { // Basic checks that don't depend on any context if (vin.empty() || vout.empty()) return error("CTransaction::CheckTransaction() : vin or vout empty"); // Check for negative values foreach(const CTxOut& txout, vout) if (txout.nValue < 0) return error("CTransaction::CheckTransaction() : txout.nValue negative"); if (IsCoinBase()) { if (vin[0].scriptSig.size() < 2 || vin[0].scriptSig.size() > 100) return error("CTransaction::CheckTransaction() : coinbase script size"); } else { foreach(const CTxIn& txin, vin) if (txin.prevout.IsNull()) return error("CTransaction::CheckTransaction() : prevout is null"); } return true; }

改一下foreach即可,至此checkblock函数已经不报错了。

11.AcceptBlock

接下来是AcceptBlock函数,这个函其实并没有报错,之前已经定义过,但是缩减版的,注释了一些代码,现在我们得恢复注释部分的功能,来看一下之前的注释:

bool CBlock::AcceptBlock() { // Check for duplicate uint256 hash = GetHash(); if (mapBlockIndex.count(hash)) return error("AcceptBlock() : block already in mapBlockIndex"); //// Get prev block index 临时注释 //map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(hashPrevBlock); //if (mi == mapBlockIndex.end()) // return error("AcceptBlock() : prev block not found"); //CBlockIndex* pindexPrev = (*mi).second; //// Check timestamp against prev //if (nTime <= pindexPrev->GetMedianTimePast()) // return error("AcceptBlock() : block's timestamp is too early"); //// Check proof of work //if (nBits != GetNextWorkRequired(pindexPrev)) // return error("AcceptBlock() : incorrect proof of work"); // Write block to history file unsigned int nFile; unsigned int nBlockPos; if (!WriteToDisk(!fClient, nFile, nBlockPos)) return error("AcceptBlock() : WriteToDisk failed"); if (!AddToBlockIndex(nFile, nBlockPos)) return error("AcceptBlock() : AddToBlockIndex failed"); //if (hashBestChain == hash) // RelayInventory(CInv(MSG_BLOCK, hash)); // // Add atoms to user reviews for coins created // vector<unsigned char> vchPubKey; // if (ExtractPubKey(vtx[0].vout[0].scriptPubKey, false, vchPubKey)) // { // unsigned short nAtom = GetRand(USHRT_MAX - 100) + 100; // vector<unsigned short> vAtoms(1, nAtom); // AddAtomsAndPropagate(Hash(vchPubKey.begin(), vchPubKey.end()), vAtoms, true); // } return true; }
12.混乱报错

PS:目前先打断一下,现在虽然processblock函数不报错,但main.h头文件里的代码全报错,报各种类型未定义。

之前没这些错的,main.h头文件也没问题的。怎么添加个ProcessBlock函数,出了这么多问题?

因为main.h里的类型,依赖的头文件,全靠其它cpp包含它时包含的,所以如果此时新建了一个cpp,只包含main.h那么,main.h里面就会报错,但此时它并没有告诉具体是哪个cpp。

而我也没新添加的cpp。

最后我发现,将main.h里的CheckTransaction函数注释掉,错误就会少很多,但这个函数并没有语法错误啊。

我们来单独一个一个编译cpp方式。

先右键net.cpp,尝试单独编译,然后报什么,CheckTransaction不是CTransaction的函数,以及报CheckTransaction函数里的CTxOut类型不存在,大概是这些吧。有些无厘头。

我记不太清了,因为我重启编译器后,这些错误都消失了,只有两个真正的报错。

就是ProcessBlock和GetNextWorkRequired找不到标识符。

这是因为我将这个两个函数的定义写在调用它的BitcoinMiner后面导致,这里我们不采取声明办法解决。

还是按源码中来,将BitcoinMiner函数的定义,直接剪切到main.cpp里的最后面就行。

好,ProcessBlock已经不报错了,GetNextWorkRequired还是一样。

点击进去,是AcceptBlock函数,将GetNextWorkRequired函数的定义放在AcceptBlock之前就行。

我猜想可能是改了代码,然后一些缓存还没更新,不同步了,然后导致的混乱报错,重启一下同步。就能找到真正的报错。问题解决就行。就不细究验证了。

至此编译正常----------------end-------

13.取消注释

我们回到AcceptBlock函数,这部分注释可以直接取消:

//// Get prev block index 临时注释 map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(hashPrevBlock); if (mi == mapBlockIndex.end()) return error("AcceptBlock() : prev block not found"); CBlockIndex* pindexPrev = (*mi).second; // Check timestamp against prev if (nTime <= pindexPrev->GetMedianTimePast()) return error("AcceptBlock() : block's timestamp is too early"); // Check proof of work if (nBits != GetNextWorkRequired(pindexPrev)) return error("AcceptBlock() : incorrect proof of work");

涉及的相关变量和函数,之前都已经定义解决了。取消注释后直接就能使用了。

好,还有另一部分:

我查了下源码,在源码中后半部分本来就是注释掉(一个已废弃的「信誉/评论市场系统」(market.h/market.cpp))。

我们保持原样即可,

然后把这个取消注释即可:

if (hashBestChain == hash) RelayInventory(CInv(MSG_BLOCK, hash));

这里注不注释都没有影响,这个功能是当这个块做为我的最优链时,就会广播一下给其它节点。

因为我没有其它节点,所以即使调用了里面的代码也不会执行。因为节点是空的。

所以我们可以取消注释,那么至此AcceptBlock里的所有代码恢复完毕,跟源码中一模一样。

好,AcceptBlock第一层已经处理完成,现在就剩AcceptBlock调用的AddToBlockIndex函数了。

这个函数包含最优链和分叉的处理。

在之前的章节中,我们让这个函数只处理创世块,接下来,我们要让它处理最优链,取消一部分注释,就是这个区块正好接到最优链上面去,让我们的第二个区块能上链,并写索引到数据库,然后更新最优链为我们的第二个区块,记录一些数据。

篇幅所限,下一章说明。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询