如何将Bitcoin比特币区块链数据导入关系数据库
在接触了比特币和区块链后,我一直有一个想法,就是把所有比特币的区块链数据放入到关系数据库(比如SQL Server)中,然后当成一个数据仓库,做做比特币交易数据的各种分析。想法已经很久了,但是一直没有实施。最近正好有点时间,于是写了一个比特币区块链的导出导入程序。
之前我的一篇博客:在区块链上表白——使用C#将一句话放入比特币的区块链上 介绍了怎么发起一笔比特币的交易,今天我们仍然是使用C#+NBitcoin,读取比特币钱包Bitcoin Core下载到本地的全量区块链数据,并将这些数据写入数据库。如果有和我一样想法的朋友,可以参考下面是我的操作过程:
一、准备
我们要解析的是存储在本地硬盘上的Bitcoin Core钱包的全量比特币数据,那么首先就是要下载并安装好Bitcoin Core,下载地址:https://bitcoin.org/en/download 然后就等着这个软件同步区块链数据吧。目前比特币的区块链数据大概130G,所以可能需要好几天,甚至一个星期才能将所有区块链数据同步到本地。当然如果你很早就安装了这个软件,那么就太好了,毕竟要等好几天甚至一个星期,真的很痛苦。
二、建立比特币区块链数据模型
要进行区块链数据的分析,那么必须得对区块链的数据模型了解才行。我大概研究了一下,可以总结出4个实体:区块、交易、输入、输出。而其中的关系是,一个区块对应多个交易,一个交易对应多个输入和多个输出。除了Coinbase的输入外,一笔输入对应另一笔交易中的输出。于是我们可以得出这样的数据模型:

需要特别说明几点的是:
1.TxId是自增的int,我没有用TxHash做Transaction的PK,那是因为TxHash根本就不唯一啊!有好几个不同区块里面的第一笔交易,也就是Coinbase交易是相同的。这其实应该是异常数据,因为相同的TxHash将导致只能花费一次,所以这个矿工杯具了。
2.对于一笔Coinbase 的Transaction,其输入的PreOutTxId是0000000000000000000000000000000000000000000000000000000000000000,而其PreOutIndex是-1,这是一条不存在的TxOutput,所以我并没有建立TXInput和TxOutput的外键关联。
3.对于Block,PreId就是上一个Block的ID,而创世区块的PreId是0000000000000000000000000000000000000000000000000000000000000000,也是一个不存在的BlockId,所以我没有建立Block的自引用外键。
4.有很多字段其实并不是区块链数据结构中的,这些字段是我添加为了接下来方便分析用的。在导入的时候并没有值,需要经过一定的SQL运算才能得到。比如Trans里面的TotalInAmount,TransFee等。
我用的是PowerDesigner,建模完成后,生成SQL语句,即可。这是我的建表SQL:
create table Block (
Height int not null,
BlkId char(64) not null,
TxCount int not null,
Size int not null,
PreId char(64) not null,
Timestamp datetime not null,
Nonce bigint not null,
Difficulty double precision not null,
Bits char(64) not null,
Version int not null,
TxMerkleRoot char(64) not null,
constraint PK_BLOCK primary key nonclustered (BlkId)
)
go /*==============================================================*/
/* Index: Block_Height */
/*==============================================================*/
create unique clustered index Block_Height on Block (
Height ASC
)
go /*==============================================================*/
/* Table: Trans */
/*==============================================================*/
create table Trans (
TxId int not null,
BlkId char(64) not null,
TxHash char(64) not null,
Version int not null,
InputCount int not null,
OutputCount int not null,
TotalOutAmount bigint not null,
TotalInAmount bigint not null,
TransFee bigint not null,
IsCoinbase bit not null,
IsHeightLock bit not null,
IsTimeLock bit not null,
LockTimeValue int not null,
Size int not null,
TransTime datetime not null,
constraint PK_TRANS primary key (TxId)
)
go /*==============================================================*/
/* Index: Relationship_1_FK */
/*==============================================================*/
create index Relationship_1_FK on Trans (
BlkId ASC
)
go /*==============================================================*/
/* Index: Trans_Hash */
/*==============================================================*/
create index Trans_Hash on Trans (
TxHash ASC
)
go /*==============================================================*/
/* Table: TxInput */
/*==============================================================*/
create table TxInput (
TxId int not null,
Idx int not null,
Amount bigint not null,
PrevOutTxId char(64) not null,
PrevOutIndex int not null,
PaymentScriptLen int not null,
PaymentScript varchar(8000) not null,
Address char(58) null,
constraint PK_TXINPUT primary key (TxId, Idx)
)
go /*==============================================================*/
/* Index: Relationship_2_FK */
/*==============================================================*/
create index Relationship_2_FK on TxInput (
TxId ASC
)
go /*==============================================================*/
/* Table: TxOutput */
/*==============================================================*/
create table TxOutput (
TxId int not null,
Idx int not null,
Amount bigint not null,
ScriptPubKeyLen int not null,
ScriptPubKey varchar(8000) not null,
Address char(58) null,
IsUnspendable bit not null,
IsPayToScriptHash bit not null,
IsValid bit not null,
IsSpent bit not null,
constraint PK_TXOUTPUT primary key (TxId, Idx)
)
go /*==============================================================*/
/* Index: Relationship_3_FK */
/*==============================================================*/
create index Relationship_3_FK on TxOutput (
TxId ASC
)
go alter table Trans
add constraint FK_TRANS_RELATIONS_BLOCK foreign key (BlkId)
references Block (BlkId)
go alter table TxInput
add constraint FK_TXINPUT_RELATIONS_TRANS foreign key (TxId)
references Trans (TxId)
go alter table TxOutput
add constraint FK_TXOUTPUT_RELATIONS_TRANS foreign key (TxId)
references Trans (TxId)
go
三、导出区块链数据为CSV
数据模型有了,接下来我们就是建立对应的表,然后写程序将比特币的Block写入到数据库中。我本来用的是EntityFramework来实现插入数据库的操作。但是后来发现实在太慢,插入一个Block甚至要等10多20秒,这要等到何年何月才能插入完啊!我试了各种方案,比如写原生的SQL,用事务,用LINQToSQL等,性能都很不理想。最后终于找到了一个好办法,那就是直接导出为文本文件(比如CSV格式),然后用SQL Server的Bulk Insert命令来实现批量导入,这是我已知的最快的写入数据库的方法。
解析Bitcoin Core下载下来的所有比特币区块链数据用的还是NBitcoin这个开源库。只需要用到其中的BlockStore 类,即可轻松实现区块链数据的解析。
以下是我将区块链数据解析为我们的Block对象的代码:
private static void LoadBlock2DB(string localPath, int start)
{
var store = new BlockStore(localPath, Network.Main);
int i = -;
BlockToCsvHelper helper = new BlockToCsvHelper(height); foreach (var block in store.Enumerate(false))
{
i++;
if (i < start)
{
continue;
} try
{
log.Debug("Start load Block " + i + ": " + block.Item.Header + " from file:" + block.BlockPosition.ToString());
var blk = LoadBlock(block, i);//将NBitcoin的Block转换为我们建模的Block对象
helper.WriteBitcoin2Csv(blk);//将我们的Block对象转换为CSV保存
}
catch (Exception ex)
{
log.Error("保存Block到数据库时异常,请手动载入,i=" + i, ex);
} }
Console.WriteLine("--------End-----------");
Console.ReadLine();
} private static Block LoadBlock(StoredBlock block, int i)
{
var blk = new Block()
{
BlkId = block.Item.Header.ToString(),
Difficulty = block.Item.Header.Bits.Difficulty,
Bits = block.Item.Header.Bits.ToString(),
Height = i,
Nonce = block.Item.Header.Nonce,
PreId = block.Item.Header.HashPrevBlock.ToString(),
TxMerkleRoot = block.Item.GetMerkleRoot().ToString(),
Size = block.Item.GetSerializedSize(),
Version = block.Item.Header.Version,
Timestamp = block.Item.Header.BlockTime.UtcDateTime,
TxCount = block.Item.Transactions.Count
};
log.Debug("Transaction Count=" + block.Item.Transactions.Count);
foreach (var transaction in block.Item.Transactions)
{
var tx = new Trans()
{
BlkId = blk.BlkId,
TxHash = transaction.GetHash().ToString(),
Version = (int)transaction.Version,
InputCount = transaction.Inputs.Count,
OutputCount = transaction.Outputs.Count,
TotalOutAmount = transaction.TotalOut.Satoshi,
TransTime = blk.Timestamp,
IsCoinbase = transaction.IsCoinBase,
IsHeightLock = transaction.LockTime.IsHeightLock,
IsTimeLock = transaction.LockTime.IsTimeLock,
LockTimeValue = (int)transaction.LockTime.Value,
Size = transaction.GetSerializedSize()
};
blk.Trans.Add(tx);
for (var idx = ; idx < transaction.Inputs.Count; idx++)
{
var input = transaction.Inputs[idx];
var txInput = new TxInput()
{
PaymentScript = input.ScriptSig.ToString(),
PaymentScriptLen = input.ScriptSig.Length,
PrevOutTxId = input.PrevOut.Hash.ToString(),
PrevOutIndex = (int)input.PrevOut.N,
Trans = tx,
Idx = idx
}; if (!tx.IsCoinbase)
{
var addr = input.ScriptSig.GetSignerAddress(Network.Main);
if (addr != null)
{
txInput.Address = addr.ToString();
}
}
if (txInput.PaymentScript.Length > )
{
log.Error("Transaction Input PaymentScript异常,将被截断,TxHash: " + tx.TxHash);
txInput.PaymentScript = txInput.PaymentScript.Substring(, );
}
tx.TxInput.Add(txInput);
}
for (var idx = ; idx < transaction.Outputs.Count; idx++)
{
var output = transaction.Outputs[idx];
var txOutput = new TxOutput()
{
Amount = output.Value.Satoshi,
ScriptPubKey = output.ScriptPubKey.ToString(),
ScriptPubKeyLen = output.ScriptPubKey.Length,
Trans = tx,
IsUnspendable = output.ScriptPubKey.IsUnspendable,
IsPayToScriptHash = output.ScriptPubKey.IsPayToScriptHash,
IsValid = output.ScriptPubKey.IsValid,
Idx = idx };
if (txOutput.ScriptPubKey.Length > )
{
log.Error("Transaction Output ScriptPubKey异常,将被截断,TxHash: " + tx.TxHash);
txOutput.ScriptPubKey = txOutput.ScriptPubKey.Substring(, );
}
if (!output.ScriptPubKey.IsUnspendable)
{
if (output.ScriptPubKey.IsPayToScriptHash)
{
txOutput.Address = output.ScriptPubKey.GetScriptAddress(Network.Main).ToString();
}
else
{
var addr = output.ScriptPubKey.GetDestinationAddress(Network.Main);
if (addr == null)
{
var keys = output.ScriptPubKey.GetDestinationPublicKeys();
if (keys.Length == )
{
//异常
log.Warn("Transaction Output异常,TxHash: " + tx.TxHash);
}
else
{
addr = keys[].GetAddress(Network.Main);
}
}
if (addr != null)
{
txOutput.Address = addr.ToString();
}
}
}
tx.TxOutput.Add(txOutput);
}
}
return blk;
}
至于WriteBitcoin2Csv方法,就是以一定的格式,把Block、Trans、TxInput、TxOutput这4个对象分别写入4个文本文件中即可。
四、将CSV导入SQL Server
在完成了CSV文件的导出后,接下来就是怎么将CSV文件导入到SQL Server中。这个很简单,只需要执行BULK INSERT命令。比如这是我在测试的时候用到的SQL语句:
bulk insert [Block] from 'F:\temp\blk205867.csv';
bulk insert Trans from 'F:\temp\trans205867.csv';
bulk insert TxInput from 'F:\temp\input205867.csv';
bulk insert TxOutput from 'F:\temp\output205867.csv';
当然在实际的情况中,我并不是这么做的。我是每1000个Block就生成4个csv文件,然后使用C#连接到数据库,执行bulk insert命令。执行完成后再把这生成的4个csv文件删除,然后再循环继续导出下一批1000个Block。因为比特币的区块链数据实在太大了,如果我不分批,那么我的PC机硬盘就不够用了,而且在导入SQL Server的时候我也怀疑能不能导入那么大批量的数据。
最后,附上一张我正在导入中的进程图,已经导了一天了,还没有完成,估计还得再花一、两天时间吧。

所有区块链数据都进入数据库以后,就要发挥一下我的想象力,看能够分析出什么有意思的结果了。
如何将Bitcoin比特币区块链数据导入关系数据库的更多相关文章
- 业内首发 | 区块链数据服务 - BDS
区块链数据服务(Blockchain Data Service,BDS)是京东云区块链产品部发推出的,其将区块链的链式.非结构化数据通过技术手段进行结构化存储,实时同步到高性能数据仓库中. 用户可以通 ...
- 安装比特币区块链钱包API(Blockchain Wallet用户发送和接收比特币的简单API)
区块链钱包API提供了一个简单的界面,商家可以用它以编程方式与钱包进行交互. 安装:要使用此API,您需要运行负责管理区块链钱包的小型本地服务. 您的应用程序通过HTTP API调用在本地与此服务进行 ...
- 一、Bitcoin比特币与BlockChain区块链技术
一.比特币历史 2008 年 10 月 31 日,一个网名叫中本聪(英文翻译过来滴)的家伙发布比特币唯一的白皮书:<Bitcoin:A Peer-to-PeerElectronic Cash S ...
- 001-Bitcoin比特币与BlockChain区块链技术
一.比特币历史 2008 年 10 月 31 日,一个网名叫中本聪(英文翻译过来滴)的家伙发布比特币唯一的白皮书:<Bitcoin:A Peer-to-PeerElectronic Cash S ...
- [区块链] 带你进入Bitcoin开发 - 环境搭建
学习完区块链枯燥乏味的.高深的.必备的基础知识后,终于可以走上开发之路了!真是迫不及待啦!之后博客更新主要放在区块链的开发上,相信这才是大多数同学更加感兴趣的地方!学习过程从最经典的区块链鼻祖-比特币 ...
- [币严BIZZAN区块链]数字货币交易所钱包对接之比特币(BTC)
在币严BIZZAN开发数字货币交易所的过程中,一共有两大难点,一个是高速撮合交易引擎,另一个是钱包对接,这两者是我们团队以前没有接触过的.这个系列的文章主要介绍数字货币交易所钱包对接实现技术.第一个要 ...
- Factom(公证通)--基于区块链的存证系统
Factom这个Solution在2014年的时候就已经推出了,现在已经2018年了,我才来写这一篇分析文章可能有些迟了,但是它是十分具有参考价值的.因为现阶段来开区块链虽然炒得火热--养猫.养狗.草 ...
- 以太坊开发DApp入门教程——区块链投票系统(一)
概述 对初学者,首先要了解以太坊开发相关的基本概念. 学习以太坊开发的一般前序知识要求,最好对以下技术已经有一些基本了解: 一种面向对象的开发语言,例如:Python,Ruby,Java... 前 ...
- 使用Go语言编写区块链P2P网络(译)(转)
转自:https://mp.weixin.qq.com/s/2daFH9Ej5fVlWmpsN5HZzw 外文链接: https://medium.com/coinmonks/code-a-simpl ...
随机推荐
- 【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析
当当当当当当当,好久不见,最近又是换工作,又是换房子,忙的不可开交,断更了一小段时间,最重要的一篇迟迟出不来,每次都犹抱琵琶半遮面,想要把它用通俗易懂的方式进行说明,确实有一定的难度,可愁煞我也,但自 ...
- Leetcode 第133场周赛解题报告
今天参加了leetcode的周赛,算法比赛,要求速度比较快.有思路就立马启动,不会纠结是否有更好的方法或代码可读性.只要在算法复杂度数量级内,基本上是怎么实现快速就怎么来了. 比赛时先看的第二题,一看 ...
- C语言检测指定文件是否存在的代码
内容之余,将做工程过程中比较常用的一些内容片段珍藏起来,下面资料是关于C语言检测指定文件是否存在的内容,希望能对小伙伴们有所用. #include <stdbool.h> #include ...
- 从零学习Fluter(二):win10上环境搭建以及模拟器和真机调试
今天呢,又继续看了flutter 弗拉特 的东西,绝的这个东西绝对是比ReactNative更高一层次的,在2018年12月5好,flutter的第一个stale1.0发布了,我们在GitHub上可以 ...
- Broadcast发送广播
一.知识介绍 1.[广播分类] ①有序广播:接收者A收到广播传递给B,B传给C,有序传递.任何一个环节都可以终止广播,也可以修改广播中携带的数据. 发送的方式:sendOrderedBroadcast ...
- 测者的测试技术笔记:揭开java method的一个秘密--巨型函数
相信,很多人都不知道Java的Method的上限为64K.本文将超过这个上限的函数叫做巨型函数. 巨型函数的问题 1.如果代码超过了这个限制,Java编译器就报"Code too large ...
- asp.net core 2.1 部署IIS(win10/win7)
asp.net core 2.1 部署IIS(win10/win7) 概述 与ASP.NET时代不同,ASP.NET Core不再是由IIS工作进程(w3wp.exe)托管,而是使用自托管Web服务器 ...
- 微信小程序下拉刷新和上拉加载的实现
一: 下拉刷新 下拉刷新两个步骤就能实现. 1.在要实现下拉刷新的页面的json配置文件里面加上 "enablePullDownRefresh": true, //开启下拉刷新 & ...
- 在本地硬盘安装WinPE系统,实现UEFI引导,摆脱U盘
之前装系统一直用U盘装PE后再装系统,这次直接想把PE系统直接装在本地某个分区中,普通的PE制作工具只能直接装在一个硬盘里没法装在某个分区,百度发现没有一篇类似的文章,只能自己想办法了.目前的PE都支 ...
- MySQL下perror工具查看System Error Code信息
在MySQL数据库的维护过程中,我们有时候会在MySQL的错误日志文件中看到一些关于Operating system error的错误信息,例如在MySQL的错误日志里面,有时候会看到关于 Inn ...