65.9K
CodeProject 正在变化。 阅读更多。
Home

NBitcoin:如何扫描区块链?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (24投票s)

2014年6月10日

CC (BY-ND 3.0)

9分钟阅读

viewsIcon

128913

一种高效且可扩展的扫描区块链的方法

警告!Scanner 在 NBitcoin 中已过时,导致“区块链扫描的简单且可扩展的设计”部分也过时。

引言

在上一篇文章中,我写了关于比特币的文章(介绍文章和一篇关于隐形地址和双因素认证的文章),我没有发明任何特别的东西,我只是想用更简单的语言解释事物是如何运作的。

另一方面,本文提出了一种简单且可扩展的区块链扫描设计,该设计适用于完整的比特币节点(以及即将推出的 SPV)。我完全发明了这个设计,目前它还处于实验阶段,尽管我编写了 100 多个单元测试,但仍可能存在一些 bug。我希望您能使用它并给我反馈。

由于是实验性的,本文的代码可能在我写它之后已经发生了一些变化。但总体原则保持不变。

对于知道比特币工作原理的人来说,您可以跳过“比特币基础知识”部分。

比特币基础知识

什么是交易

对于不熟悉比特币的人,我将解释它的内部工作原理,以便您能够理解本文的其余部分,对于熟悉的人,请跳过。

正如您可能听说过的,比特币的目标是成为一种去中心化的货币。去中心化意味着任何人都可以在网络验证、确认传播交易,而无需任何许可和身份验证。

但什么是交易?交易是一种原子操作,将比特币从发送方转移到接收方。(一笔交易可能涉及多方)

发送方填写交易的输入部分(按惯例在顶部)。接收方填写交易的输出部分(按惯例在底部)。

输入包含对先前输出的引用,称为 OutPoint,以及来自发送方的所有权证明,称为 scriptSig(通常,但不限于,ECDSA 签名,如我在本文中所解释的)。

输出包含一个 scriptPubKey,它指定了接收方证明其所有权以花费资金的方式。输出还包含要花费的 BTC 金额。

这是一个 Yellow 支付给 Green (1 BTC)、Blue (10 BTC)、Orange (5 BTC) 的例子。

image

现在想象一下这笔交易在网络上被确认,Orange 想支付 4 BTC 给 Green。在比特币中,您不能部分消耗一个输出。您一次性将其全部消耗。

因此,之前的 Orange 输出引用输入。输出包含支付给 Green 的 4BTC,以及支付回 Orange 的 1 BTC。

image

现在,如果这笔交易在网络上被确认,Green 想支付 9 BTC 给 Blue。同样,Green 将在新的交易中将其两个输出引用为输入。并将剩余金额退还给自己。

image

交易如何验证

去中心化意味着任何人都可以自行验证交易的有效性。您需要验证什么?

  • 每个输入都引用一个未花费输出
  • 每个输入都正确地证明了对所引用输出的所有权(如我在本文中所解释的)。
  • 已花费输出的总和(由输入的 outpoint 引用的输出)等于或大于输出的总和。这两个金额之间的差额称为费用

因此,如果您想验证交易的有效性,那么您需要从一开始(目前大约 20GB)的所有已确认交易,并索引所有未花费输出,以便有效地检查引用的输出是否已被花费。

交易如何确认

交易由矿工确认。矿工是一种在经济上受到激励的代理,他们确认在网络上传播的交易。

什么激励着矿工?

  • 矿工会获得他确认的所有交易的所有费用。
  • 矿工会获得新创建的 BTC 金额,称为奖励。目前金额为 25 BTC,但将来会减少,直到达到 2100 万 BTC 的最大发行量。(参见此页面。)

矿工将他想要确认的交易收集到一个区块中,并且该区块的哈希值需要以特定数量的零开头,交易才能被网络“确认”。

零的数量称为“难度”,难度每两周根据区块的挖掘速度进行调整,因此您可以预期每 10 分钟在世界范围内发现一个区块。

如何即时确认交易

有些人认为比特币确认交易所需的时间太长,与 Visa 等相比。这根本不是真的。您不必等待这么长时间就能确定付款人不会试图多次花费他的输出。

正如前一篇文章中所解释的,您可以创建一个只能通过两个不同密钥正确签名才能花费的输出。

如果买方和商户有一个共享的信任方,该信任方将与买方共同签名一个输入,那么商户只需验证交易是否由信任方签名,并信任该信任方不会签署另一笔交易来花费相同的输出。

什么是区块链

区块链是自创建以来比特币网络中所有已发现区块的有序列表。因此,它是所有已确认交易的集合。

如何获取余额

您需要扫描整个区块链以查找未花费的输出,而您知道如何证明所有权。如何简单地扫描?这正是我接下来的文章要讨论的内容。

(已过时) 区块链扫描的简单且可扩展的设计

扫描器

如前所述,扫描器是一个处理区块链中交易的类,以查找您感兴趣的未花费输出。

以下是一些扫描器的想法,其中一些是我开发的,一些是我将要开发的

  • 彩色币扫描器
  • PubKeyHashScanner(经典比特币地址)
  • PubKeyScanner(与上面相同,但 pubkey 用于 scriptPubKey 而不是其哈希)
  • StealthPaymentScanner(使用扫描密钥接收您的资金,兼容 Dark wallet,如本文中所述)

扫描器提供两件事:如果可能,提供一个您想在交易中查找的数据模式,通过 GetScannedPushData,这可以大大缩短扫描时间,因为您无需下载大部分不感兴趣的区块(感谢 BIP 37)。

ScanCoins,它接收一个区块及其高度,并输出一组 ScannedCoin(这只是一个被剥离了扫描器无关数据的交易)。

image

扫描器是无状态的,因此我们将讨论如何保存扫描器的进度。

ScanState

image

如果您不想在每次重新启动程序时都扫描区块链,您需要存储两种数据,我将它们封装在 ScanState 类中。

  • 到目前为止您已扫描的 Chain
  • 以及给您带来未花费输出集和当前余额的 Account

您的程序将有一个完整的 mainChain,它将反映当前的 BlockChain
但是,每个 ScanState 都有自己的,可以是部分的(如果您今天创建一个 Key,您不想从 2008 年至今扫描整个区块链)。

image

每次您想扫描 mainChain 时,都会调用 Process 方法,并传递一个 IBlockProvider
该方法将

  • Chain 是分叉的情况下取消 Account 中的一些 AccountEntry
  • 遍历链,从 mainChainScanState.Chain 之间的最新分叉开始,同时更新 AccountChain

但是 AccountChain 可能会变得非常大,所以为了提高效率,需要增量保存 AccountChain。用于此目的的方法是将 Account 和 Chain 保存为更改流。(有些人称这种设计为事件溯源)。每次 Account 或 Chain 的状态更改时,它都会保存为更改,在反序列化过程中会重新播放。

负责保存这种增量更改的类称为 ObjectStream

image

ChainAccount 都使用它(分别是 ObjectStream<ChainChange>ObjectStream<AccountEntry>)。

image

目前唯一的实现是 StreamObjectStream<T>,它将您的更改保存到 Stream 中。(文件流或内存流)。

让我们回到 ScanState.Process

image

ChainBlockHeader 的集合,因此它不包含 Block 的交易。这意味着扫描器需要一种方法来从 BlockHeader 获取 Block。这就是 IBlockProvider 的作用。

您可以将 searchedData 传递给 IBlockProvider.GetBlock,这样,当我实现 SPV 时,区块提供商将只下载包含带有 searchedData 的推送的区块。

目前,只有一个实现需要您计算机上的完整区块链:IndexedBlockStore

image

IndexedBlockStore 需要两样东西:一个名为 NoSqlRepository 的键值存储(唯一存在的实现是 SQLiteNoSqlRepository)。

以及一个 BlockStore,它是一个原始的、可枚举的已下载区块集合。它使用的格式与bitcoinq相同。(blkXXX.dat)

总之,以下是如何索引您自己的 IndexedBlockStore

var store = new BlockStore("bitcoin/datadir", Network.Main);
var index = new SQLiteNoSqlRepository("myindex.db", createNew: true);
var indexedStore = new IndexedBlockStore(index, store);
indexedStore.ReIndex(); //Can take a while (300 000+ blocks)

ScanState.Process 需要第二样东西:mainChain,它是当前的完整 BlockHeader 链。您可以通过使用 NodeServer 从比特币网络中非常轻松地获取此链。

NodeServer 这个名字有点误导,因为它只有在您调用 Listen() 时才是一个服务器,否则它只是比特币网络上的一个普通客户端节点。

NodeServer client = new NodeServer(Network.Main);
var inFile = new StreamObjectStream<ChainChange>
             (File.Open("MainChain.dat",FileMode.OpenOrCreate));
var mainChain = client.BuildChain(infile);

在此示例中,NodeServer.BuildChain

  • 通过重放更改,从给定的 ObjectStream<ChainChange> 构建链。
  • 从链和已下载链的最后一个分叉下载所有 BlockHeader(如果下载当前 300,000 个区块头,则需要 1 分钟)。

手动扫描

如果您想跟踪区块链上有趣的转账,以上所有扫描都很好。但是想象一下您想做一个简单的分析应用程序?

例如,在某些情况下,我需要做一个查询来检索 2012 年 12 月到 2013 年 1 月之间,金额约为 1100 BTC 的所有交易。

首先,让我们获取所有交易的链(1 分钟),您只需要做一次,因为链已保存到 *MainChain.dat*。

var node = Node.ConnectToLocal(Network.Main);
node.VersionHandshake();
var chain = node.GetChain();

然后,让我们构建所有区块的索引,以便之后我们可以按 blockId 查询区块。

BlockStore store = new BlockStore("E:\\Bitcoin\\blocks", 
Network.Main); //the folder is the blocks folder of all blocks saved by Bitcoin-QT, 
 //the original bitcoin client application, this "store" can only browse blocks sequencially
IndexedBlockStore index = 
new IndexedBlockStore(new SQLiteNoSqlRepository("PlayIndex"), store); //Save a SqlLite 
                                                     //index in a file called PlayIndex
index.ReIndex(); //Index, to do one time only to fill the index

一旦拥有了链和索引,您就可以运行分析了。

var headers =
    chain.ToEnumerable(false)
        .SkipWhile(c => c.Header.BlockTime < from)
        .TakeWhile(c => c.Header.BlockTime < to)
        .ToArray();

var target = Money.Parse("1100");
var margin = Money.Parse("1");
var en = new CultureInfo("en-US");

FileStream fs = File.Open("logs", FileMode.Create);
var writer = new StreamWriter(fs);
writer.WriteLine("time,height,txid,value");

var lines = from header in headers
            let block = index.Get(header.HashBlock)
            from tx in block.Transactions
            from txout in tx.Outputs
            where target - margin < txout.Value && txout.Value < target + margin
            select new
            {
                Block = block,
                Height = header.Height,
                Transaction = tx,
                TxOut = txout
            };
foreach(var line in lines)
{
    writer.WriteLine(
        line.Block.Header.BlockTime.ToString(en) + "," +
        line.Height + "," +
        line.Transaction.GetHash() + "," +
        line.TxOut.Value.ToString());
}
writer.Flush();

下一步?

NBitcoin 的下一步将是为 SPV 场景创建 IBlockProvider。它将使用 NodeServerIndexedBlockStore 缓存和 BloomFilter 来下载扫描器所需的区块。这样的 BlockProvider 将是线程安全的,因此多个扫描器可以同时下载区块,同时共享相同的缓存。

此外,NBitcoin 缺乏一个连贯的钱包模型,该模型重用了 ScanState 的概念,但这很快就会到来。

结论

本文中没有大量的代码,但我希望您通过我编写的 150 多个单元测试来发现和使用这个库。(再次重申,我还移植了比特币核心的测试。)

这个库花了我大量的时间来编码,所以非常乐意收到您的反馈或改进建议,或者打赏到 15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe,这样我就可以保持动力,继续吃披萨喝咖啡!

历史

  • 2014 年 6 月 10 日:初始版本
  • 2021 年 3 月 5 日:更新代码
© . All rights reserved.