基础介绍
本文档介绍了如何使用0.14.7中的代码,git master可能略有不同
bitcoinj是在Java 7环境下运行的,同时可以使用该版本JVM支持的任意语言。 本教程适用于Java和JavaScript,但你同样可以在Python、Scala、Clojure、Kotlin、Ruby等等中使用bitcoinj,大多数流行的编程语言都可以在JVM运行。
请注意,这些教程都是需要你熟悉比特币协议的基础知识。 如果您还不熟悉区块链的结构以及区块链交易交易的工作方式,请在阅读本教程之前阅读中本聪的白皮书。
安装步骤
我们可以直接下载jar包依赖,可以使用Maven/Gradle来导入相应依赖,或者直接clone下它的源码bitcoinj使用Maven作为其构建系统并通过git分发。您可以使用源代码/ jar下载,但直接从源代码库获取它会更安全。
本人是直接基于源码来编译,bitcoinj项目结构如下
好了,已经可以跑起来了,我们来运行下源码中提供的钱包
运行结果:
动手之前
这个库不像我们其他的SDK库,你可以直接通过比特币的API来操作BTC资产,如果做生产应用的话这可能是其他人的一大笔钱。理解其内容很重要。就算学习了本教程,您也不具备编写生产应用程序的资格。但是我们可以知道在比特币这个微妙而复杂的系统上,如何去编写这样一个应用。
如果不能完全的去了解熟悉比特币系统的相关知识,您可能导致货币被盗用或者被永久的销毁
这个文档能帮助您学习如何使用botcoinj,但是并不是很全面。如果您有任何疑问或者能发现一些代码上的问题,反馈给其官网或者社区。
基本结构
bitcoinj内置了SLF4J库来进行日志管理,同时你可以选择自己喜欢的日志系统来打印日志,例如JDK logging,Android logging等等。默认情况下使用简单的日志记录器stderr来输出日志,我们可以切换lib目录中的jar文件来选择新的日志记录器。
一个bitcoinj的应用使用以下对象:
- NetworkParameters 来选择当前的网络(测试/生产)
- Wallet 来存储 ECKey 和其他的一些数据
- PeerGroup 网络连接的管理
- BlockChain 用于管理让比特币工作的共享全局数据结构,区块链的数据单元结构
- BlockStore 将块链数据结构保存在某个位置,例如存储在磁盘。
- WalletEventListener 它接收钱包的响应事件
为了简化设置,还有一个 WalletAppKit 对象,用于创建上述对象并将它们连接在一起。 虽然可以手动执行此操作(对于大多数“真实”应用程序),这里我们就只演示如何使用应用程序的这个工具包。
你可以在这里阅读完整的程序,现在来看看代码,看看它是如何工作的。
起步
bitcoinj使用一个工具函数来初始化配置log4j,使其具有更加紧凑和简洁的日志格式,然后检查了命令行的参数:1
2
3
4
5BriefLogFormatter.init();
if (args.length < 2) {
System.err.println("Usage: address-to-send-back-to [regtest|testnet]");
return;
}
然后我们根据可选的命令行参数选择我们要使用的网络: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";
}
从上面可以看出,有多个独立的比特币网络:
- MainNetParams 用户生产交易的真实网络
- TestNet3Params 公共测试网络,会不定时去重置,让我们可以使用新功能
- RegTestParams 回归测试模式,它不是公共网络,需要您自己运行带有-regtest标志的比特币守护进程。
每个网络都有自己的创世区块,自己的端口号和自己的地址前缀字节,以防止您误操作通过网络发送数字货币(这将不起作用)。 这些设置被封装到一个 NetworkParameters 的单例对象中。 正如你所看到的,每个网络都有它自己的Params类,并且通过在其中一个对象上调用 get() 来获取相关的 NetworkParameters 对象。
强烈建议您在测试网上开发您的软件或使用regtest模式。 如果您不小心遗失了测试的货币也没关系,您可以从TestNet Faucet免费获得大量测试货币。 请务必在完成后将货币重新送回TestNet Faucet,以便其他人也可以使用它们。
在regtest模式下,没有公共基础设施,但是您可以随时获得新区块。
密钥和地址
比特币交易其实就是向一个地址发送资金。交易发起人创建一个包含接受者地址的交易,其中地址是其公钥哈希的编码形式。然后收件人用他们自己的私钥签署一项接受货币的交易。一个密钥用 ECKey 类体现。 ECKey可以包含私钥,或者仅包含缺少私钥的公钥。请注意,在椭圆曲线密码学中,公钥是从私钥导出的,所以知道私钥本身就意味着知道公钥。这与您可能熟悉的一些其他加密系统(如RSA)不同。
地址是公钥的文本编码。实际上,它是公钥的160位哈希值,具有版本字节和一些校验的字节,使用比特币专用编码方式,称为 Base58 ,将其编码为文本。 Base58的设计目的是为了避免字母和数字在写下来时可能会相互混淆,例如 1 和大写字母 i 。1
2// Parse the address given as the first parameter.
forwardingAddress = new Address(params, args[0]);
因为一个地址编码密钥将用于的网络,所以我们需要在这里传递一个网络参数。 第二个参数只是用户提供的字符串。 如果传入不可解析的参数或错误的网络,构造函数就会抛出异常。
钱包应用组件
bitcoinj由不同层次组成,每个层次的运行等级都低于上一层。想要开发一个最基础的发送和接收资金的应用程序,至少需要一个 BlockChain ,一个 BlockStore ,一个 PeerGroup 和一个 Wallet 。这些对象都需要互相连接,以便数据正确传输。有关数据如何通过基于bitcoinj的应用程序的更多信息,请阅读“bitcoin不同组件结合”。
为了简化这个过程,这通常相当于样板,我们提供了一个名为 WalletAppKit 的高级封装。它以简化的付款验证模式配置bitcoinj(而不是全面验证),除非您是专家并且希望尝试(不完整的,可能有问题的)完整模式,否则这是目前最适合选择的模式。它提供了一些简单的properties和hooks,可以让您修改默认的配置。
将来,可能会有更多的工具包针对可能有不同需求不同类型的应用程序来配置bitcoinj。但现在只有一个O__O…。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) {
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();
该工具包有三个参数:
- NetworkParameters(库中的几乎所有API都需要此参数)
- File 一个用于存储文件的目录
- String 一个可选字符串,该字符串以任何已创建的文件为前缀。 如果您在同一个目录中有多个不同的bitcoinj应用程序,您希望保持分离状态,这非常有用。 在这种情况下,如果不是主网络的话文件前缀是“forwarding-service”加上网络名称,具体请参阅上面的代码。
它同时提供了一个可重写的方法,在这我们可以自定义一些代码。不过注意的是,AppKit实际上是在后台创建和初始化对象的,因此 onSetupCompleted 也会在后台线程中调用。
这里,我们只需检查钱包是否至少有一个密钥,如果没有,我们将添加一个新密钥。 如果我们从磁盘加载一个钱包,这个代码路径不会被采用。
接下来,我们检查我们是否使用regtest模式。 如果是的话,那么我们就告诉组件只连接到本地主机,当然在该主机会运行一个bitcoind。
最后,我们调用 kit.startAsync() 。 WalletAppKit 是一个 Guava服务。 Guava是Google提供的广泛使用的实用程序库,它为标准Java库增加了一些有用的附加功能。 服务是一个可以启动和停止(但只有一次)的对象,并且您可以在完成启动或关闭时收到回调。 您也可以阻塞线程,它以 awaitRunning() 开始,这就是我们在这里所做的。
WalletAppKit 会在块链完全同步时考虑自己启动,有时可能需要一段时间。 您可以去了解如何更快地实现这一步骤,但对于测试演示的应用程序,我们就不需要实施任何额外的优化了。
该组件提供访问它配置的基础对象。只有在类被启动或者在启动的过程中,你才能调用这些。
应用程序启动后,您会注意到应用程序运行的目录中有两个文件:一个.wallet文件和一个.spvchain文件,它们必须要形影不离的。
事件处理
我们想知道我们的地址在什么时候收到钱以至于我们可以很快的知道去处理它相关的业务。 这是一个事件,bitcoinj与大多数Java API一样,您可以通过注册事件侦听器了解事件,事件侦听器只是实现接口的对象。 库中有一些事件监听器接口:
WalletEventListener - 适用于您的钱包触发的一些事件
BlockChainListener - 用于与块链相关的事件
PeerEventListener - 用于与网络中的对等方相关的事件
TransactionConfidence.Listener - 用于一个事务所具有的回滚安全级别相关的事件
大多数应用程序不需要使用所有这些监听器。 因为每个接口都提供了一组相关的事件,你可能不关心所有的事件。1
2
3
4
5
6kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) {
// Runs in the dedicated "user thread".
}
});
bitcoinj中的事件在专用后台线程中运行,这些线程仅用于运行事件侦听器,称为用户线程。 这意味着它可以与应用程序中的其他代码并行运行,并且如果您正在编写GUI应用程序,则意味着您不允许直接修改GUI,因为您不在GUI或“主”线程中。 但是,您的事件侦听器本身并不需要线程安全,因为事件将按顺序排队并执行。 您也不必担心使用多线程库时通常会出现的其他许多问题(例如,安全的 re-enter 和 blocking 操作)。
编写GUI应用程序的相关说明
像Swing,JavaFX或Android等大多数构件工具组件都具有所谓的线程关联性,这意味着您只能从单个线程使用它们。 要从后台线程回到主线程,通常需要将闭包传递给某个实用程序函数,该函数在GUI线程闲置时调度运行闭包。
为了简化使用bitcoinj编写GUI应用程序的任务,您可以在注册事件侦听器时指定任意的Executor。 该执行者将被要求运行事件监听器。 默认情况下,这意味着将指定的Runnable传递给用户线程,但您可以重写如下所示:1
2
3
4
5
6
7
8
9
10
11Executor runInUIThread = new Executor() {
public void execute(Runnable runnable) {
SwingUtilities.invokeLater(runnable); // For Swing.
Platform.runLater(runnable); // For JavaFX.
// For Android: handler was created in an Activity.onCreate method.
handler.post(runnable);
}
};
kit.wallet().addEventListener(listener, runInUIThread);
现在,“侦听器”上的方法将自动在UI线程中调用。
但是这会变得重复和繁琐,所以您还可以更改默认执行程序,以便所有事件始终在您的UI线程上运行:1
Threading.USER_THREAD = runInUIThread;
在某些情况下,bitcoinj可以非常快速地生成大量事件;典型的应用场景,当块链与一个拥有大量交易的钱包同步时,每交易都可以触发交易信息改变事件(因此他们被埋得更深,更深)。未来很有可能钱包事件的工作方式会改变以避免这个问题,但现在这就是API的工作原理。如果用户线程落后,则随着事件侦听器调用在堆上排队,内存移除可能会发生。为了避免这种情况,可以使用 Threading.SAME_THREAD 作为executor注册事件处理程序,在这种情况下,它们将立即在bitcoinj控制的后台线程上运行。但是,在使用这种模式时,你必须特别小心:代码中发生的任何异常都可能导致bitcoinj堆栈退出并导致对等连接断开,同样,重新进入库可能导致锁定倒置或其他问题。一般来说,你应该避免这样做,除非你真的需要额外的表现,并确切知道你在做什么。
接收货币
1 | kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() { |
在这里我们可以看到当我们的应用程序收到钱时会发生什么,我们打印出我们收到了多少,使用静态工具方法将其格式化为文本。
然后我们做一些更先进的事情。 我们调用这个方法:1
ListenableFuture<TransactionConfidence> future = tx.getConfidence().getDepthFuture(1);
每个交易都有一个与之相关的Confidence对象。 Confidence概念包含了这样一个事实,即比特币是一个全球共识系统,它不断努力就全球交易顺序达成一致。 因为当面对恶意行为者时,这是一个难以解决的问题,交易可能会花费双倍(在bitcoinj术语中,我们说它是“dead”)。 也就是说,我们有可能信任了我们已经收到了钱,后来我们发现世界其他地方与我们不一致。
Confidence 对象包含了我们可以用来做出基于风险的决策数据,以确定我们实际上收到了多少钱。 我们也可以通过它来得知Confidence发生变化或达到某个阈值。
Futures 是并发编程中的一个重要概念,bitcoinj大量使用它们,特别是Guava基于标准Java Future类做了扩展,它被称为 ListenableFuture。 ListenableFuture代表未来计算或状态的结果。 您可以等待它完成(阻止调用线程),或注册将被调用的回调。 Futures 运行失败的情况下,你会得到一个异常而不是结果。
在这里,我们要求一个 depth future 。当交易被链中的许多区块埋没时,这个Futures就会完成。一个深度会出现在链条的顶部区域。因此,在这里,我们说“当交易至少有一次确认时运行此代码”。通常情况下,您会调用 Futures.addCallback 方法,还有另一种注册侦听器的方法,可以在下面的代码片段中看到。
然后,当发送货币的交易确认时,我们只需调用我们自己定义的forwardCoins方法。
这里有一件重要的事情需要注意。depth future可能运行,然后交易深度变化小于future的参数。这是因为在任何时候比特币网络都可能进行“重组”,其中最被共识认可的链从一个切换到另一个。如果您的交易出现在另一个地方的新链中,则深度可能实际上会下降,而不是上升。在处理入站付款时,您应确保如果交易的Confidence下降,您会尝试中止您为该笔资金提供的任何服务。通过阅读SPV安全模型,您可以了解有关此主题的更多信息。
处理 链重组 和 双重花费 是本教程未涉及的一个复杂主题。你可以通过阅读其他文章来了解更多。
发送货币
ForwardingService的最后一部分是发送我们刚刚收到的货币。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Coin 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() {
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 方法。 它有三个参数:一个 TransactionBroadcaster(通常是一个 PeerGroup),发送货币d的地址(这里我们使用我们之前从命令行解析的地址)以及发送多少钱。
sendCoins 返回一个包含已创建交易的 SendResult 对象,以及一个可用于查明网络何时接受付款的 ListenableFuture。 如果钱包没有足够的钱,sendCoins方法会抛出一个异常,其中包含有关缺少多少钱的一些信息。
自定义发送过程并设置费用
比特币交易可以附加费用。 这作为反拒绝服务机制是有用的,但它主要是为了在通胀下降(毕竟比特币总量是固定的)时激励系统后期的采矿。 您可以通过自定义发送请求来控制附加到交易的费用:1
2
3
4SendRequest req = SendRequest.to(address, value);
req.feePerKb = Coin.parseCoin("0.0005");
Wallet.SendResult result = wallet.sendCoins(peerGroup, req);
Transaction createdTx = result.tx;
请注意,在这里我们实际上为创建的交易设置了每千字节的费用。 这就是比特币的工作原理 - 交易的优先级由费用除以大小决定,因此较大的交易需要较高的费用才能与小型交易“相同”。
结尾语
bitcoinj中还有很多其他的功能,本教程没有介绍。 您可以阅读其他文章以了解有关完整验证,钱包加密等的更多信息,当然JavaDoc会详细介绍完整的API。