实现访问Bitcoin核心的JSON-RPC接口

万事开头难啊,经历了几天的反复试错和各种尝试终于成功实现了JSON-RPC接口,获取Bitcoin的数据。可以开开心心的获取数据进行分析研究了,下面记录一下我的心路历程。

比特币开发者的 API

首先明确我的目是要获取Bitcoin区块链中的数据。一开始理所当然的想,会不会有第三方的接口可以直接调取,得到这些数据呢?这非常符合一个程序员的思维(其实就是懒)。于是我找到了 blockchain.info,果然里面提供了丰富的获取区块链数据的工具:

  • Blockchain钱包API
    • Blockchain 钱包 API。用API从 Blockchain 的钱包账户来发送和接收付款。
    • Bitcoin-Qt 兼容的 JSON RPC。Blockchain.info 能够协助商户使用钱包账户从事商业活动的比特币化 RPC 服务器。
    • 创建钱包。以编程方式为您的用户创建钱包,您亦能加载和兑换资金。
  • 在交易记录和区块上的数据
    • Blockchain 数据 API。查询 json 区块上的数据和交易。您在此网站看到的几乎所有的数据都可用以 JSON 格式。
    • 简单查询 API。为查询区块链数据的简单纯文本 API。
    • Websocket。低延迟流套接字通道提供了新的区块和交易数据。订阅新区块、交易记录、某个地址和接收JSON描述某个事件发生时的交易或区块的消息。

并且Blockchain.info提供多种语言的 官方API库。可以说到此,我们已经完美的解决了如何获取Bitcoin区块链中的数据这一目标。但是好奇的我在想能不能自己本地也起一个RPC-Server,这样即使离线也可以获取区块数据进行数据分析。那么接下来目标就改为如何运行一个本地的RPC-Server并从中获取区块链数据。

运行Bitcoin完全节点

我最终采用的是Go语言实现的bitcoin完全节点 btcsuite。由于比较心疼我的笔记本,所以找了一台的曙光的服务器起了一个虚拟来运行btcd。

  • 笔记本 MacBook Pro OS X Yosemite, 用做实现客户端
  • 服务器 曙光 虚拟机 Ubuntu 14.04, 用做运行Bitcoin完全节点

首先需要在服务器安装go,安装指导详见 http://golang.org/doc/install

安装完后可以确认是否安装成功,顺便记录一下我的安装路径:

1
2
3
4
5
$ go version
go version go1.7.4 linux/amd64
$ go env GOROOT GOPATH
/usr/local/go
/home/demontf/blockchain/golang

GOROOTGOPATH最好不要是同一个路径,然后记得把$GOPATH/bin添加到系统PATH中。下一步安装btcd。

1
2
3
4
5
$ go get -u github.com/Masterminds/glide
$ git clone https://github.com/btcsuite/btcd $GOPATH/src/github.com/btcsuite/btcd
$ cd $GOPATH/src/github.com/btcsuite/btcd
$ glide install
$ go install . ./cmd/..

安装完后在$GOPATH/bin下面会多出addblockbtcctlbtcdfindcheckpointgencerts这几个命令。使用 xx –help可以方便的了解使用方法。

btcd 比较常用,参考手册

在控制台运行btcd &,就会发现一个bitcoin的全节点成功运行起来了,但接下来是一个漫长的过程,100G+的区块链数据花了我近一周的时间才下载完成,存储路径:/home/demontf/.btcd/data/mainnet/ peers.json中记录了所有节点信息。blocks_ffldb中存储了所有区块数据。

1
2
3
4
5
6
7
8
9
~/.btcd/data/mainnet/blocks_ffldb$ ls -lh
total 96G
-rw-rw-r-- 1 demontf demontf 512M Jan 19 17:46 000000000.fdb
-rw-rw-r-- 1 demontf demontf 512M Jan 19 18:14 000000001.fdb
-rw-rw-r-- 1 demontf demontf 512M Jan 19 17:22 000000002.fdb
-rw-rw-r-- 1 demontf demontf 512M Jan 19 17:37 000000003.fdb
-rw-rw-r-- 1 demontf demontf 512M Jan 19 18:30 000000004.fdb
-rw-rw-r-- 1 demontf demontf 512M Jan 19 19:07 000000005.fdb
...

配置开启RPC-Server

btcd默认是关闭RPC服务的, 需要在/home/demontf/.btcd/btcd.conf中作如下配置才能开启。

1
2
3
4
5
[Application Options]
rpcuser=myuser
rpcpass=SomeDecentp4ssw0rd
rpclimituser=mylimituser
rpclimitpass=Limitedp4ssw0rd

RPC服务默认监听8334端口,更多配置详情可以参考 RPC服务配置
btcd 按照Bitcoin API 标准调用列表(版本0.8.0) 兼容实现各个接口,并对每个接口是否访问安全设置了user和limituser。

btcd采用TLS提供基于HTTP POSTWebSockets的API访问方式。相比较于HTTPWebSockets拥有三个优势:允许一个连接中有多个请求,支持异步通知,大规模发送请求。但是,HTTP的方式在我的经历中更加常见,我也更加熟悉。Btcd 除了提供了 标准API 之外,针对WebSockets设计了 扩展API

完成配置之后,再启动btcd就会发现RPCServer 已经启动并监听8334端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
demontf@ubuntu:~/.btcd$ btcd
12:23:05 2017-02-07 [INF] BTCD: Version 0.12.0-beta
12:23:05 2017-02-07 [INF] BTCD: Loading block database from '/home/demontf/.btcd/data/mainnet/blocks_ffldb'
12:23:05 2017-02-07 [INF] BTCD: Block database loaded
12:23:05 2017-02-07 [WRN] CHAN: Unknown new rules activated (bit 0)
12:23:05 2017-02-07 [INF] CHAN: Chain state (height 451824, hash 000000000000000000e879c5a7bcbfd0005e43b751f10c8253cb17be829cf4b9, totaltx 194051683, work 74090852964515113230545583)
12:23:05 2017-02-07 [INF] RPCS: RPC server listening on 127.0.0.1:8334
12:23:05 2017-02-07 [INF] RPCS: RPC server listening on [::1]:8334
12:23:05 2017-02-07 [INF] AMGR: Loaded 13318 addresses from file '/home/demontf/.btcd/data/mainnet/peers.json'
12:23:05 2017-02-07 [INF] CMGR: Server listening on [::]:8333
12:23:05 2017-02-07 [INF] CMGR: Server listening on 0.0.0.0:8333
12:23:06 2017-02-07 [INF] CMGR: 40 addresses found from DNS seed seed.bitnodes.io
12:23:06 2017-02-07 [INF] CMGR: 25 addresses found from DNS seed seed.bitcoinstats.com
12:23:06 2017-02-07 [INF] CMGR: 13 addresses found from DNS seed bitseed.xf2.org
12:23:06 2017-02-07 [INF] BMGR: New valid peer [2001:470:1f15:11f8::10]:8333 (outbound) (/Satoshi:0.13.2/)
12:23:06 2017-02-07 [INF] BMGR: Syncing to block height 451894 from peer [2001:470:1f15:11f8::10]:8333

访问Client的实现

btcd 提供了两个官方的实现都是针对WebSocketsGo版本Nodejs版本。按照官网指导可以正确的实现获取数据的功能,基本没有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var fs = require('fs');
var WebSocket = require('ws');

// Load the certificate for the TLS connection which is automatically
// generated by btcd when it starts the RPC server and doesn't already
// have one.
var cert = fs.readFileSync('rpc.cert');
var user = "demontf";
var password = "*****";

// Initiate the websocket connection. The btcd generated certificate acts as
// its own certificate authority, so it needs to be specified in the 'ca' array
// for the certificate to properly validate.
var ws = new WebSocket('wss://127.0.0.1:8334/ws', {
headers: {
'Authorization': 'Basic '+new Buffer(user+':'+password).toString('base64')
},
cert: cert,
ca: [cert]
});
ws.on('open', function() {
console.log('CONNECTED');
// Send a JSON-RPC command to be notified when blocks are connected and
// disconnected from the chain.
// get most recent block hash
ws.send('{"jsonrpc":"1.0","id":"getbestblockhash","method":"getbestblockhash","params":[]}');

});
ws.on('message', function(data, flags) {
//console.log(data);
var res = JSON.parse(data);
var flag = res.id;

switch(flag){
case "getbestblockhash":
console.log("run getbestblockhash:");
//console.log(res.result);
ws.send('{"jsonrpc":"1.0","id":"getblock","method":"getblock","params":["'+res.result+'",true,true]}');
break;
case "getblock":
console.log("run getblock");
//console.log(res.result);
var previousblockhash = res.result.previousblockhash;
var confirmations = res.result.confirmations;
var size = res.result.size;
var height = res.result.height;
var version = res.result.version;
var merkleroot = res.result.merkleroot;
var time = res.result.time;
var nonce = res.result.nonce;
var bits = res.result.bits;
var difficulty = res.result.difficulty;
var nextblockhash = res.result.nextblockhash;
var txs = res.result.rawtx;

ws.send('{"jsonrpc":"1.0","id":"decoderawtransaction","method":"decoderawtransaction","params":["'+txs[0]['hex']+'"]}');
break;
case "getrawtransaction":
console.log("run getrawtransaction:");
console.log(res);
break;
case "getpeerinfo":
console.log(res);
break;
case "decoderawtransaction"://解析交易
console.log("run decoderawtransaction:");
var vin = res.result['vin'];
var vout = res.result['vout'];
console.log(vin);
console.log(vout);
break;
default:console.log("run default");
}
});
ws.on('error', function(derp) {
console.log('ERROR:' + derp);
})
ws.on('close', function(data) {
console.log('DISCONNECTED');
})

起初我尝试用nodejs的方式获取并分析数据。注意需要使用
npm install ws安装websocket。 由于对nodejs的不熟悉感觉用起来总是怪怪的不舒服,最终决定使用Java来实现。这一过程中遇到了不少坑。

起初为了挑战没有选择用Http协议实现,选择陌生的WebSocket协议也算是学习啦。

WebSocketClientEndpoint.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.demontf;

/**
* Created by demontf on 17/2/6.
*/

import sun.misc.BASE64Encoder;

import javax.websocket.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


@ClientEndpoint
public class WebsocketClientEndpoint {

Session userSession = null;
private MessageHandler messageHandler;

public WebsocketClientEndpoint(URI endpointURI) {

try {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
ClientEndpointConfig config = ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
List<String> arrayList = new ArrayList<String>();
String s = new BASE64Encoder().encode("demontf:123456".getBytes());
arrayList.add("Basic " + s);
headers.put("Authorization", arrayList);

}
}).build();

container.connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
System.out.println("inner opening websocket");
}
}, config, endpointURI);


} catch (Exception e) {
throw new RuntimeException(e);
}
}


/**
* Callback hook for Connection open events.
*
* @param userSession the userSession which is opened.
*/
@OnOpen
public void onOpen(Session userSession) {
System.out.println("opening websocket");
this.userSession = userSession;
}

/**
* Callback hook for Connection close events.
*
* @param userSession the userSession which is getting closed.
* @param reason the reason for connection close
*/
@OnClose
public void onClose(Session userSession, CloseReason reason) {
System.out.println("closing websocket");
this.userSession = null;
}

/**
* Callback hook for Message Events. This method will be invoked when a client send a message.
*
* @param message The text message
*/
@OnMessage
public void onMessage(String message) {
if (this.messageHandler != null) {
this.messageHandler.handleMessage(message);
}
}

/**
* register message handler
*
* @param msgHandler
*/
public void addMessageHandler(MessageHandler msgHandler) {
this.messageHandler = msgHandler;
}

/**
* Send a message.
*
* @param message
*/
public void sendMessage(String message) {
this.userSession.getAsyncRemote().sendText(message);
}

/**
* Message handler.
*
* @author Jiji_Sasidharan
*/
public static interface MessageHandler {

public void handleMessage(String message);
}


}

测试T.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.demontf;

import java.net.URI;
import java.net.URISyntaxException;


/**
* Created by demontf on 17/2/6.
*/
public class T {

public static void main(String[] args){
try {
// open websocket
System.out.println("s");//wss://real.okcoin.cn:10440/websocket/okcoinapi
final WebsocketClientEndpoint clientEndPoint = new WebsocketClientEndpoint(new URI("wss://127.0.0.1:8334/ws"));

// add listener
clientEndPoint.addMessageHandler(new WebsocketClientEndpoint.MessageHandler() {
public void handleMessage(String message) {
System.out.println(message);
}
});

// send message to websocket
clientEndPoint.sendMessage("{'jsonrpc':'1.0','id':'getbestblockhash','method':'getbestblockhash','params':[]}");

// wait 5 seconds for messages from websocket
Thread.sleep(5000);

} catch (InterruptedException ex) {
System.err.println("InterruptedException exception: " + ex.getMessage());
} catch (URISyntaxException ex) {
System.err.println("URISyntaxException exception: " + ex.getMessage());
}

}


}

一开始写Java版WebSocket实现的时候,我以为和Nodejs一样需要在Header中添加Certification,结果后台一直返回Unknown certificate,并且在文档中也没有查到传certification的key名称用什么。在Go中用的是Certification在Nodejs中用的是Cert。

进过一番研究才知道Java提供了密钥和证书管理工具Keytool。SSL(Secure Sockets Layer,安全套接层)/TLS(Transport Layer Security,传输层安全)保证了客户端和web服务器的连接安全。客户端通过HTTPS连接使用web资源。为创建与客户端的安全连接,以加密格式发送/接受信息,Java提供了完善的安全体系API类库。

  • JCA(Java Cryptography Architecture,Java加密体系结构)
  • JCE(Java Cryptographic Extension,Java加密扩展包)
  • JSSE(Java Secured Socket Extension,Java安全套接字扩展包)

SSL连接要求web服务器持有数字证书,该证书使客户端信任该web应用的可靠性。需要发送加密信息的应用从CA(Certificate Authority,数字证书认证机构)申请数字证书。CA验证应用所有者的详细信息和其他身份信息,并签发数字证书。
在PKI(Public Key Infrastructure,公钥基础设施)体系中,数字证书由CA签发,它包括识别名(DN,Distinguished Name)/所有者名称/使用者(Subject),唯一识别证书的序列号,所有者公钥,签发日期,到期时间,CA识别名,签发机构(CA)的数字签名,签名的创建算法。CA签发的数字证书发布在CA的注册库中,这样认证用户就可以使用所有者的公钥。

在/home/user/.btcd/目录下 已经生成了数字证书rpc.cert。如果本机挖矿那么需要将btcd RPC的证书添加到系统的CA列表里。

1.拷贝 rpc.cert 到 /usr/share/ca-certificates:

1
$cp /home/user/.btcd/rpc.cert /usr/share/ca-certificates/btcd.crt
  1. 添加 btcd.crt 到 /etc/ca-certificates.conf:
1
$echo btcd.crt >> /etc/ca-certificates.conf
  1. 更新 CA 列表:
1
$update-ca-certificates

在Nodejs和Go中需要将证书读取并添加近请求头中随着请求一起发送

GO中

1
2
3
4
5
6
7
8
9
10
11
...
certs, err := ioutil.ReadFile(filepath.Join(btcdHomeDir, "rpc.cert"))

connCfg := &btcrpcclient.ConnConfig{
Host: "localhost:8334",
Endpoint: "ws",
User: "yourrpcuser",
Pass: "yourrpcpass",
Certificates: certs,
}
...

Nodejs中

1
2
3
4
5
6
7
8
9
10
11
12
...
var fs = require('fs');
var cert = fs.readFileSync('/path/to/btcd/appdata/rpc.cert');

var ws = new WebSocket('wss://127.0.0.1:8334/ws', {
headers: {
'Authorization': 'Basic '+new Buffer(user+':'+password).toString('base64')
},
cert: cert,
ca: [cert]
});
...

将证书添加到 cacerts 存储

参考文章 将证书添加到 Java CA 证书存储

  1. 然而在java中需要将证书颁发机构 (CA) 证书添加到 Java CA 证书 (cacerts) 存储。
    在设置为 JDK 的 jdk\jre\lib\security 文件夹的命令提示符下,运行以下命令可查看将安装的证书:

    1
    keytool -list -keystore cacerts
  2. 系统将提示你输入存储密码。默认密码为 changeit。(如果您想要更改密码,请参阅 keytool 文档,网址为 http://docs.oracle.com/javase/7/docs/technotes/tools/windows/keytool.html。) 此示例假定 MD5 指纹为 67:CB:9D:C0:13:24:8A:82:9B:B2:17:1E:D1:1B:EC:D4 的证书未列出,并且你想要导入该证书(这是 Twilio API 服务所需的特定证书)。

  3. 获取 GeoTrust 根证书上列出的证书列表中的证书。右键单击序列号为 35:DE:F4:CF 的证书的链接,并将该证书保存到 jdk\jre\lib\security 文件夹。在此示例中,该证书已保存到名为 Equifax_Secure_Certificate_Authority.cer 的文件。
    通过以下命令导入证书:

    1
    keytool -keystore cacerts -importcert -alias equifaxsecureca -file Equifax_Secure_Certificate_Authority.cer

当系统提示信任此证书时,如果证书的 MD5 指纹为 67:CB:9D:C0:13:24:8A:82:9B:B2:17:1E:D1:1B:EC:D4,请通过键入 y 进行响应。

  1. 运行以下命令可确保已成功导入 CA 证书:

    1
    keytool -list -keystore cacerts
  2. 压缩 JDK 并将其添加到 Azure 项目的 approot 文件夹。

有关 keytool 的信息,请参阅 http://docs.oracle.com/javase/7/docs/technotes/tools/windows/keytool.html。

意外的惊喜

当所有坑都趟完,不论是直接访问在线的API接口还是使用自己的RPC-Server,都可以很方便的获取区块链中的数据。我惊奇的发现在github上有个大牛封装了一套支持Java语言实现的基于HTTP协议的JSON-RPC接口,简直欣喜若狂,果断成为github的搬运工将其运转了起来,目前来看效果良好并且省的自己再封装,其实现逻辑值得学习。

btcd-cli4j

使用心得后面慢慢再总结吧~

如果你觉得有帮助,欢迎鼓励。

Bitcoin: 1F4W7beHieYtubSc3ngrozWz5QkRMnMh4J
PPC: PRfRev7MLYdVyBtN8hpaBfPDcG558trjc8

坚持原创技术分享,记录点滴成长历程!