bitcoinj的介绍:
bitcoinj提供了一个与Bitcoin协议交互的类库。bitcoinj维护了一个钱包(wallet),以及发送/接收交易(transactions)的方法,此外还维护了许多其他的功能特点,将会在下面一一列举。
Feature 1: 高度优化的简单支付验证(simplified payment verification,既SPV)。在这种模式下,只有一部分区块链会被下载到本地,这允许了bitcoinj运行在一些轻量级的设备上,例如智能手机或者是个人虚拟网络中。
Feature 2:维护了一个加密的Wallet类,可以通过这个类进行费率计算,加签,生成秘钥(Key),插件化的比特币选择,持有比特币,这个类支持扩展,并且可以添加事件监听器。(后面会单独介绍这个类的)
Feature 3:提供了一个简单的GUI Wallet 应用,可以通过这个自定义属于你自己的app
Feature 4:提供一套实验性的完整验证模式,这个模式完成了和比特币核(Bitcoin core)一样的验证工作。在这个模式中,未被消费的交易(transaction)输出集合会被计算出来, 并且你可以通过PostgreSQL存储,用索引的方式通过地址(address)回溯整个账本的余额。
Feature 5:支持小额交易渠道(micropayment channels),可以通过这个构建客户端和服务器端的多重签名的合约(multi-signature contract),然后就在这种渠道上进行更快速的小额交易,并且避免了费率问题。
bitcoinj是由Java语言实现的,但是可以被运行再JVM上的其他语言所使用,例如Python或者 JavaScript。
bitcoinj 的组件
bitcoinj 内置了日志(logging)和断言(assertions)。bitcoinj可以通过Maven或者Gradle的方式来进行构建。
一个bitcoinj应用包含了一下一些模块:
NetworkParameters 实例,提供了一个可选的生产或测试的网络环境
Wallet实例,保存 ECKeys 和其他的数据
PeerGroup实例,管理网络连接
BlockChain实例,维护了让Bitcoin运行的一个全局的共享的数据结构
BlockStore实例,存储了Block Chain的数据结构,例如存放在硬盘上
WalletEventListener的实现,监听Wallet 事件
一个基本的应用包括了以上几个模块,bitcoinj还提供了一个简化的 WalletAppKit 类,这个类创建了以上所有的对象,并且连接了各个模块的功能。
现在我们开始使用 WalletAppKit 构建一个程序
首先,使用工具类的方法配置log4j
1 BriefLogFormatter.init(); 2 if (args.length < 2) { 3 System.err.println("Usage: address-to-send-back-to [regtest|testnet]"); 4 return; 5 }
然后通过可选的命令行参数来选择需要的网络环境
// 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"; }
bitcoinj提供了三个可供选择的网络
主网络(Main)或者生产(Production)网络,是人们购买或贩卖东西的地方
公共测试网络(testnet)是实时重置的,可以在这个环境中进行新功能的测试
回归测试网络 (regtest),这个环境不是公有的,需要你通过 -regtest 标志运行一个bitcoin的deamon线程
没个网络都拥有一个创世纪块(genesis block, 就是网络中的第一个块),初始块拥有自己的端口号和地址前缀字节,这避免了偶然性的跨网络的操作。这些对象都是封装在一个 NetworkParameters 单例对象中,可以通过 get() 方法获取对应的对象实例。
值得注意的是,在regtest模式下,可以通过运行 "bitcoind -regtest setgenerate true" 命令,随时随地产生一个新块 (block),而不需要长时间等待,只需要regtest模式下 bitcoinj 守护进程 (bitcoind )仍在运行中。
接下来是秘钥和网络(Keys and addresses)
bitcoin 交易一般把金钱发送到一个公共的椭圆曲线键中( public elliptic curve key)(方法到这里的目的是为了加密吧~~)。交易发起方先创建一个包含了接收方地址的交易,这个地址是接收方的公钥的hash的加密密文,然后接收方用他们的秘钥验签这个交易并声明了对应的比特币。而 ECKey 类就代表了这个过程中的Key。ECkey包含了秘钥,或者是包含了缺少秘钥部分的公钥。 Note : 椭圆曲线加密学中(elliptic curve cryptography),公钥是由私钥产生的,所以知道了密钥也就知道了公钥,这和其他一些加密算法入 RSA 不同的地方。
一个地址(Addresses)是一个文本格式的公钥。实际上,它是包含了一个版本(version)位和检验(checksum)位的一个160位的公钥的hash,这个hash是用为Bitcoin专门设计的base58编码格式编码的。
// Parse the address given as the first parameter. forwardingAddress = new Address(params, args[0]);
因为一个地址(address)标识了一个Key将要被使用的网络,所以在这个方法中传入了network参数。第二个参数只是一个用户传入的参数(没啥用~~)
正如之前所说的,一个bitcoinj由许多不同的层次组件构成,每一个都运行在比上一层更低的层次中。一个典型的应用,能实现发送和接收金币的功能,至少要包括一个区块链(BlockChain),区块存储(BlockStore),用户组(PeerGroup),和一个钱包(Wallet),所有的这些组件需要链接在一起以保证数据的正常流动。对此,Bicoinj 提供了一个更高级的封装类 WalletAppKit 。这个类用简单支付验证模式(simplified payment verification)的方式,配置了bitcoinj,提供了一些简单的属性和入口让你修改默认的配置选项。
// 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().getKeyChainGroupSize() < 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();
kit的构造方法有三个参数,第一个是代表网络环境对象的 NetworkParameter,基本上所有的bitcoinj中的APIs都需要用到这个参数,第二个是存储文件的目录,第三个是一个可选的字符串类型的参数,这个字符串将被用于所有被创建的文件的文件名的前缀,如果在同一个目录下有多个 bitcoinj 应用,这个参数就回很有作用。
构造方法也提供了一个可以被重写的方法,可以通过这个方法来自定义创建的对象。Note : appkit是在一个后台线程中创建对象,因此,onSetupCompleted 方法也是在后台线程中被调用的。
在这里,我们只是检查了Wallet至少拥有一个Key,如果没有,则导入一个新的Key。如果我们从硬盘中加载一个Wallet,则这段代码将不会被执行。
接着,检查了是否使用的是regtest模式,如果是,则告诉 kit 对象去连接有一个正在运行的regtest中的bitcoin守护进程(bitcoind)的本地环境。
最后,调用 kit.startAsync() 方法。 WalletAppKit 是一个 Guava服务。一个服务(Service)是一个可以被启动和暂停的对象,并且你可以回调来响应服务启动或关闭事件。在这里,只是简单的阻塞了调用线程直到线程开始调用 awaitRunning() 方法。
在区块链(block chain)完全同步后,WalletAppKit就算是启动了,这个过程可能会持续一段时间,但是可以通过一些方法进行优化。
Kit对象可以访问到它配置的一些底层的对象,但是在这个类启动前或者正在启动的时候不能调用这些对象(对进行assert判断),因为这个时候底层的对象还没有被创建。
在app启动后,会创建app的启动目录下新建两个文件:.wallet 和 .spvchain。 这两个文件是在一起的,不能被分开。
响应事件 (Handling Event)
bitcoinj 提供了一些事件监听器接口来帮助什么时候接受到比特币并对其进行转发。
WalletEventListener:监听 Wallet 中发生的事件
BlockChainListener:监听区块链中的相关事件
PeerEventListener:监听网络中其他节点(peer)的事件
TransactionConfidence.Listener: 监听有关交易的回滚安全级别的事件
大多数应用并不需要所有的这些监听器,应为有些监听事件你并不用去关心。
kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() { @Override public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) { // Runs in the dedicated "user thread". } });
需要注意的事,所有的事件监听器都运行在一个专门的用于监听事件的后台线程中。这意味着监听器是和其他代码是并行的,并且如果你正在开发一个GUI应用,是不能直接访问修改GUI的,因为监听并不在GUI线程或是在主线程(main thread)中。然而,所有的事件监听器是不用去考虑线程安全问题的,因为事件监听器是会排队有序执行的。
接收金币
kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() { @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<TransactionConfidence>() { @Override public void onSuccess(TransactionConfidence result) { // "result" here is the same as "tx" above, but we use it anyway for clarity. forwardCoins(result); } @Override public void onFailure(Throwable t) {} }); } });
这里,我们通过添加监听器的方式处理接收到金钱时的处理逻辑,这里只是格式化并打印接收到金币的数量。
然后我们调用了一个方法进行进一步的操作
ListenableFuture<TransactionConfidence> future = tx.getConfidence().getDepthFuture(1);
每一个交易都会关联一个信任(confidence)对象。比特币是一个全球共识系统,致力于实现全球交易顺序一致的目标。这不是一个简单的问题,一个交易可能会被二次消费(double spent),导致交易作废。我们可能确信我们收到了钱,但是后来发现被不被世界认可。(这里会涉及到区块链中的共识算法 POW, POS, DPOS等,以及为什么确认交易后要等到当前块后面有继续出现其他块的时候才能确认交易)。
信任(Confidence)对象包含了一些数据,根据这些数据我们可以用来做风险决策,决定我们有多大的可能能收到金币。它也可以用于帮助我们了解交易风险的变化,以及是否达到我们的风险阈值。
Futures是在bicoinj中很重要的一个类对象。bitcoinj使用了Guava拓展了标准的Java Future类,称为ListenableFuture。ListenableFuture代表了一个Future的计算或者是一个状态。可以通过阻塞的方式等待这个对象返回结果或者是通过注册回调的方式获取返回的结果。Note: ListenableFuture 会失败,此时得到的结果将会是一个异常而不是你想要的结果。
在这里我们请求了一个depth future对象,当交易被至少在当前链中的所有块后后生成时,这个future就算是完成了。深度是1,代表了当前是链中的第一块。因此,这段代码可理解为,当交易至少被确认一次后才会运行到这段代码。更常见的方式是使用工具类方法,Futures.addCallback。 但是还有其他的方式注册监听回调,将在下面涉及到。
在这段代码接下来的方法中,当发送给我们的交易被确认的时候,我们调用了自定义的方法 forwcoins 发 转发当前交易。
Note: 有个问题值得一提,当一个 depth future 运行时,交易的 depth 可能会变得比方法中的参数更小。这是因为在任何时刻,Bitcoin网络都可能会经历一次重组(reorganisation),重组可能导致主链的切换,变为另一条链,此时你的交易出现在另一条链中,并且depth可能是下降而不是上升。此时,你就应该选择放弃你的交易了。
发送比特币
最后一步就是要将我们接收到的金币发送出去
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()); } });
这段代码中,首先查询了我们接收到的金币金额。随后我们将决定要发送多少钱出去 ——这个数值是我们所得到的金额减去一个手续费。
这里手续费的作用是为了更快的进行交易确认。
这里使用了Wallet 类的 sendCoins 方法进行金币的发送。这个方法有三个参数,第一个是 TransactionBroadcaster (通常是一个PeerGroup),第二个是要发送金币的地址(这里使用的是从命令行解析的地址),第三个参数是要发送的金额。
sendCoins 方法返回了一个 SendResult 对象,这个对象包含了创建的交易,以及一个ListenableFuture,这个future包含了网络什么时候接受这个支付的信息。如果没有足够的金额,sendCoins方法会抛一个异常,异常信息包括缺少多少金额的信息。
自定义发送过程并且设定费率
比特币的交易可以包含费率。这个机制可以用来作为DOS的防御措施,但最初主要是为了激励系统采矿当近几年的通货膨胀率下降时。可以通过自定义一个 SendRequest的方法来改变交易的费率。
SendRequest req = SendRequest.to(address, value); req.feePerKb = Coin.parseCoin("0.0005"); Wallet.SendResult result = wallet.sendCoins(peerGroup, req); Transaction createdTx = result.tx;
Note :这里设置的是创建的交易每一千个字节的费率。这就是比特币怎么工作的 —— 交易的优先级决定于交易的大小除以费率,因此,大交易需要更高的费率才可以达到和小交易一样的优先级。
译文自官方文档 : https://bitcoinj.github.io/getting-started-java#initial-setup