Bitcoinj学习笔记之开始使用

Java版 Bitcoin

bitcoinj支持java版和JavaScript版,本文讲解的是java版的相关细节。本文参考bitcoinj官方文档

1.基本结构


bitcoinj主要使用的对象

  • NetworkParameters 选择生产或测试环境网络
  • Wallet 用于存储ECKey以及其他数据
  • PeerGroup管理网络链接
  • BLockChain管理共享的、全局的数据结构
  • BlockStore保存区块链上的数据结构,如光盘一样
  • WalletEventListener 接收钱包事件
  • WalletAppKit可以方便的创建和使用上面几个对象

    2.安装


设置日志系统

1
2
3
4
5
BriefLogFormatter.init();
if (args.length < 2) {
System.err.println("Usage: address-to-send-back-to [regtest|testnet]");
return;
}

选择网络

  • main或者“production”网络用与人们买卖交易
  • public test network(testnet)用于测试实验,可以研究新特性
  • 回归测试模式(regtest)不是一个共有网络,需要自己启动daemon
1
2
3
4
5
6
7
8
9
10
11
12
13
//Figure out which network we should connect to. Each one gets its own set of files.
NetworkParameters params;
String filePrefix;
if (args[1].equals("testnet")) {
params = TestNet3Params.get();
filePrefix = "forwarding-service-testnet";
} else if (args[1].equals("regtest")) {
params = RegTestParams.get();
filePrefix = "forwarding-service-regtest";
} else {
params = MainNetParams.get();
filePrefix = "forwarding-service";
}

每一个网络都拥有起始的区块、独立的端口号和地址前缀。这些都封装在NetworkParameters单例对象中,使用时调用call()方法。值得一提的是,在testnet中可以免费从TestNet Faucet获取大量的币。在regtest模式中不存在公有的设施,我们可以使用bitcoind -regtest setgenerate true产生新的区块。

3.密钥和地址

比特币交易通常是将钱发送到一个由椭圆曲线生成的公钥。发送者生成一个交易,交易中包含有接受者的地址。接受者的地址是由公钥经过hash得到的。关于私钥公钥地址的关系我总结如下:

  • k私钥(CSPRNG随机产生,256bit二进制数,64位16进制表示)
  • K公钥(私钥 通过椭圆曲线相乘产生,20字节160bit)
  • 公钥哈希(公钥通过HSA256和RIPEMD160处理得到,20字节160bit)
  • 比特币地址(公钥哈希通过Base58check编码得到)

椭圆曲线算法可以从私钥计算得到公钥,这是不可逆的过程。K=k*G,其中k是私钥,G是被称之为生成点的常数点。K是所得公钥,不可以从公钥计算得到私钥,此运算被称为“寻找离散对数”问题,难以解决。

密码学公式:

\(y^2 mod p=(x^3+7))mod p\)

上述的mod p(素数p取模),表明该曲线是在素数阶p的有限域内也写作 Fp,其中 \(p=2^{256}-2^{32}-2^9-2^8-2^7-2^6-2^4-1\) (这是一个非常大的素数)

4.Wallet App 工具

bitcionj有很多不同的层组成,每层的级别都低于后一层。在一个典型的操作场景中,一方想发送金额给另一方时至少需要使用到BlockChainBlockStorePeerGroupWallet。关于这几个对象间如何正确的连接并交换数据的内容,下一篇文章再详述。

往往有很多模板可以简化这个过程,bitcoinj提供了一个高级别的封装类名为WalletAppKit。它设置bitcoinj为SPV模式(与之相对的是全校验模式),可以提供一些简单的属性和默认配置的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Start up a basic app using a class that automates some boilerplate. Ensure we always have at least one key.
kit = new WalletAppKit(params, new File("."), filePrefix) {
@Override
protected void onSetupCompleted() {
// This is called in a background thread after startAndWait is called, as setting up various objects
// can do disk and network IO that may cause UI jank/stuttering in wallet apps if it were to be done
// on the main thread.
if (wallet().getKeychainSize() < 1)
wallet().importKey(new ECKey());
}
};

if (params == RegTestParams.get()) {
// Regression test mode is designed for testing and development only, so there's no public network for it.
// If you pick this mode, you're expected to be running a local "bitcoind -regtest" instance.
kit.connectToLocalHost();
}

// Download the block chain and wait until it's done.
kit.startAsync();
kit.awaitRunning();

这个工具需要传入三个参数

  • NetworkParameters 几乎所有的API都会需要
  • 文件存储目录
  • 文件存储前缀(选填)

我们可以重写onSetupCompleted方法,加入自己的代码。bitcoinj会为他起一个线程后台运行。接着检查钱包是否至少拥有一个key,如果没有就刷新一个新的key。下一步检查是否使用了regtest模式,如果是那么链接localhost。最后调用kit.startAsync()

5.处理事件

bitcoinj中的事件监听和我们平时大多数使用的监听一样,对象需要实现一个如下几个接口:

  • WalletEventListener 钱包中发生的事件
  • BlockChainListener 区块链相关的事件
  • PeerEventListener 关于网络中节点的事件
  • TransactionConfidence.Listener 交易安全回滚级别相关的事件

对于大部分的应用来说不需要使用全部这些接口,因为每个接口中都定义了很多相关的事件。使用的时候我们只需实现抽象接口即可。Bitcoinj中后台的user thread专门负责运行事件监听。

1
2
3
4
5
6
kit.wallet().addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) {
// Runs in the dedicated "user thread".
}
});

6.接收币

设置监听,实现onCoinReceived方法并传入四个参数,示例中打印出收到的金额,设置Future回调

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
kit.wallet().addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) {
// Runs in the dedicated "user thread".
//
// The transaction "tx" can either be pending, or included into a block (we didn't see the broadcast).
Coin value = tx.getValueSentToMe(w);
System.out.println("Received tx for " + value.toFriendlyString() + ": " + tx);
System.out.println("Transaction will be forwarded after it confirms.");
// Wait until it's made it into the block chain (may run immediately if it's already there).
//
// For this dummy app of course, we could just forward the unconfirmed transaction. If it were
// to be double spent, no harm done. Wallet.allowSpendingUnconfirmedTransactions() would have to
// be called in onSetupCompleted() above. But we don't do that here to demonstrate the more common
// case of waiting for a block.
Futures.addCallback(tx.getConfidence().getDepthFuture(1), new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction result) {
// "result" here is the same as "tx" above, but we use it anyway for clarity.
forwardCoins(result);
}

@Override
public void onFailure(Throwable t) {}
});
}
});

因为比特币是一个需要全球交易顺序达成一致的全球共识系统,所以每个交易都需要有一个置信对象(confidence object)。置信对象需要处理成功和失败两种情况,因为很可能我们自己认为的已经收到了金额,但是稍后发现全球其他人并不认同。我们可以使用置信对象包括的数据做出风险决策、计算我们实际收到钱的可能性、了解共识变化。

Futures是并发编程的一个重要概念,在bitcoinj中大量使用。bitcoinj使用Guava扩展了标准的JavaFuture类,生成ListenableFutureListenableFuture可以获取一些future的计算和状态,可以等待执行结束也可以注册回调。Futures失败的时候会收到一个异常。

当交易被确认后调用forwardCoins

7.发送币

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Coin value = tx.getValueSentToMe(kit.wallet());
System.out.println("Forwarding " + value.toFriendlyString() + " BTC");
// Now send the coins back! Send with a small fee attached to ensure rapid confirmation.
final Coin amountToSend = value.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
final Wallet.SendResult sendResult = kit.wallet().sendCoins(kit.peerGroup(), forwardingAddress, amountToSend);
System.out.println("Sending ...");
// Register a callback that is invoked when the transaction has propagated across the network.
// This shows a second style of registering ListenableFuture callbacks, it works when you don't
// need access to the object the future returns.
sendResult.broadcastComplete.addListener(new Runnable() {
@Override
public void run() {
// The wallet has changed now, it'll get auto saved shortly or when the app shuts down.
System.out.println("Sent coins onwards! Transaction hash is " + sendResult.tx.getHashAsString());
}
});

首先我们查询收到了多少金额(同样也可以通过onCoinsReceived中的newBalance获得),接着确定要发送的金额等同于收到的金额,设置交易手续费为最低,这样可能会延长确认时间。

最后调用sendCoins,该方法会返回一个SendResult对象其中包括创建的交易和ListenableFuture

交易手续费一方面可以用来防止拒绝服务另一方面可以用来激励矿工。用户可以自定义每笔交易的手续费。

1
2
3
4
5
SendRequest req = SendRequest.to(address, value);
//kb kilobyte 每千字节
req.feePerKb = Coin.parseCoin("0.0005");
Wallet.SendResult result = wallet.sendCoins(peerGroup, req);
Transaction createdTx = result.tx;
坚持原创技术分享,记录点滴成长历程!