bitcoinj是一个使用比特币协议的库。它可以维护钱包,发送/接收交易而无需比特币核心的本地副本,并具有许多其他高级功能。它是用Java实现的,但可以通过任何JVM兼容语言中使用:包括Python和JavaScript中的示例。
它附带完整的文档,并建立了许多大型,众所周知的比特币应用程序和服务。下面我们来看看如何使用它。
bitcoinj内置了日志记录和断言。无论是否指定了-ea标志,都会默认检查断言。记录由 SLF4J
库处理。它允许你选择你更喜欢使用的日志系统,例如JDK日志记录,Android日志记录等。默认情况下,我们使用简单的logger来打印 stderr
感兴趣的大部分内容。你可以通过切换lib目录中的jar文件来选择一个新的logger。
bitcoinj使用Maven作为其构建系统,并通过git分发。你可以使用源代码/ jar下载,但直接从源存储库获取它更安全。
要获取代码并安装它,请抓取 Maven 或 Gradle ,并将其添加到你的路径中。还要确保安装了git。可能你的Java IDE也有一些Maven/Gradle和Git集成,但是通过命令行使用它们还是非常有用。
现在获取最新版本的代码。你可以使用 使用Maven 或 使用Gradle 页面上的说明——只需在那里运行命令,你就可以获得正确的代码版本(除非此网站本身已被泄露)。这是为了防止受损镜像或源代码下载——因为git使用源树哈希工作,如果以正确的方式获得源哈希,则可以保证最终得到正确的代码。
你可以在 这里 阅读完整的程序。
bitcoinj应用程序使用以下对象:
NetworkParameters
实例,用于选择你所在的网络(生产或测试)。 ECKeys
和其他数据的 Wallet
实例。 PeerGroup
实例。 BlockChain
实例,它管理共享的全局数据结构,使比特币工作。 BlockStore
实例,它将块链数据结构保存在某个位置,就像在磁盘上一样。
为了简化设置,还有一个 WalletAppKit
对象可以创建上述对象并将它们连接在一起。虽然可以手动执行此操作(对于大多数“真实”应用程序),但此演示应用程序会显示如何使用应用程序工具包。
让我们看看代码,看看它是如何工作的。
我们使用实用程序函数将log4j配置为具有更紧凑,更简洁的日志格式。然后我们检查命令行参数。
BriefLogFormatter.init(); if (args.length < 2) { System.err.println("Usage: address-to-send-back-to [regtest|testnet]"); return; }
然后我们根据可选的命令行参数选择我们将要使用的网络:
// 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
单例对象中。如你所见,每个网络都有自己的类,你可以通过在其中一个对象上调用 get()
来获取相关的 NetworkParameters
对象。
强烈建议你在testnet上或使用regtest模式开发软件。如果你不小心丢失了测试比特币,这没什么大不了的,因为它们毫无价值,你可以从 TestNet Faucet 免费获得大量的比特币。确保在完成后将比特币送回水龙头,以便其他人也可以使用它们。
在regtest模式下,没有公共基础设施,但是你可以随时获得一个新的块而不必等待一个通过在regtest模式bitcoind运行的同一台机器上运行 bitcoind -regtest setgenerate true
。
比特币交易通常将钱汇入公共椭圆曲线键。发件人创建包含收件人地址的交易,其中地址是其公钥哈希的编码形式。接收者然后签署一个交易,用他们自己的私钥声明比特币。密钥用 ECKey
类表示。 ECKey
可以包含私钥,或只包含缺少私有部分的公钥。请注意,在椭圆曲线加密中,公钥是从私钥派生的,因此知道私钥本身也意味着知道公钥。这与你可能熟悉的其他一些加密系统不同,例如RSA。
地址是公钥的文本编码。实际上,它是公钥的160位hash,具有版本字节和一些校验和字节,使用名为base58的比特币特定编码编码到文本中。Base58旨在避免在写下时可能相互混淆的字母和数字,例如1和大写i。
// Parse the address given as the first parameter. forwardingAddress = new Address(params, args[0]);
由于地址对要为其使用密钥的网络进行编码,因此我们需要在此处传递网络参数。第二个参数只是用户提供的字符串。如果构造函数不可解析或者网络错误,它将抛出钱包应用套件例外。
bitcoinj由各种层组成,每层都在比最后一层更低的层次上运行。想要发送和接收资金的典型应用程序至少需要 BlockChain
, BlockStore
, PeerGroup
和 Wallet
。所有这些对象需要相互连接,以便数据正确流动。阅读 如何融合在一起
,了解有关数据如何通过基于bitcoinj的应用程序流动的更多信息。
为了简化这个过程(通常是样板文件),我们提供了一个名为 WalletAppKit
的高级打包器。它在简化的支付验证模式(而不是完全验证)中配置bitcoinj,这是此时选择的最合适的模式。除非你是专家并且希望尝试(不完整的,可能是错误的)完整模式,它提供了一些简单的属性和钩子,允许你修改默认配置。
将来,可能会有更多的工具包为不同类型的应用程序配置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();
该工具包有三个参数 - NetworkParameters(几乎所有库中的API都需要这个),一个用于存储文件的目录,以及一个以任何创建文件为前缀的可选字符串。如果你希望保持分隔的同一目录中有多个不同的bitcoinj应用程序,这将非常有用。在这种情况下,文件前缀是“forwarding-service”加上网络名称,如果不是主网络(参见上面的代码)。
它还提供了一个可覆盖的方法,我们可以将自己的代码放入其中,以自定义它为我们创建的对象。我们在这里覆盖它。请注意,appkit实际上将在后台线程上创建和设置对象,因此也会从后台线程调用onSetupCompleted。
在这里,我们只需检查钱包是否至少有一个密钥,如果没有,我们会添加一个新密钥。如果我们从磁盘加载钱包,那么当然不会采用此代码路径。
接下来,我们检查我们是否使用regtest模式。如果我们是,那么我们告诉套件只连接到本地主机,其中预计会在regtest模式下运行bitcoind。
最后,我们调用kit.startAsync()。 WalletAppKit是一种番石榴服务。 Guava是Google广泛使用的实用程序库,它增加了标准Java库以及一些有用的附加功能。服务是一个可以启动和停止的对象(但只能启动一次),并且可以在完成启动或关闭时接收回调。你也可以阻止调用线程,直到它以awaitRunning()启动,这就是我们在这里所做的。
当块链完全同步时,WalletAppKit将认为自己已经启动,这有时需要一段时间。你可以了解如何加快速度,但对于玩具演示应用程序,不需要实现任何额外的优化。
该工具包上有访问器,可以访问它配置的底层对象。在类启动或启动过程之前,你不能调用它们(它们将断言),因为不会创建对象。
应用程序启动后,你会注意到应用程序运行的目录中有两个文件:.wallet文件和.spvchain文件。他们走在一起,决不能分开。
我们想知道什么时候收到钱,所以我们可以转发它。这是一个交易,与bitcoinj中的大多数Java API一样,你通过注册事件侦听器 event listeners
来了解交易,事件侦听器只是实现接口的对象。库中有一些交易监听器接口:
WalletEventListener BlockChainListener PeerEventListener 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". } });
bitcoinj中的交易在专用的后台线程中运行,该线程仅用于运行事件侦听器,称为 user thread
用户线程。这意味着它可以与应用程序中的其他代码并行运行,如果你正在编写GUI应用程序,则意味着你不能直接修改GUI,因为你不在GUI或 main
主线程中。但是,事件侦听器本身不需要是线程安全的,因为交易将按顺序排队并执行。你也不必担心使用多线程库时通常会出现的许多其他问题(例如,重新进入库是安全的,并且可以安全地执行阻塞操作)。
大多数小工具工具包(如Swing,JavaFX或Android)都具有所谓的线程关联,这意味着你只能在单个线程中使用它们。要从后台线程返回到主线程,通常会将闭包传递给某个实用程序函数,该函数调度在GUI线程空闲时运行的闭包。
为了简化使用bitcoinj编写GUI应用程序的任务,你可以在注册事件侦听器 listener
时指定任意 Executor
。将要求该执行程序运行事件侦听器。默认情况下,这意味着将给定的 Runnable
传递给用户线程,但你可以像这样覆盖:
Executor runInUIThread = new Executor() { @Override 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);
现在, listener
上的方法将自动在UI线程中调用。
因为这可能会重复且烦人,你还可以更改默认执行程序,因此所有交易始终在你的UI线程上运行:
Threading.USER_THREAD = runInUIThread;
在某些情况下,bitcoinj可以非常快速地生成大量交易,这在将块链与具有大量交易的钱包同步时是典型的,因为每个交易都可以生成交易可信度 confidence
更改交易(因为它们隐藏的很深)。未来钱包交易的工作方式很可能会改变以避免这个问题,但是现在这就是API的工作方式。如果用户线程落后,则当事件侦听器 listener
调用在堆上排队时,可能会发生内存膨胀。为避免这种情况,你可以使用 Threading.SAME_THREAD
作为执行程序注册交易处理程序,在这种情况下,它们将立即在bitcoinj控制的后台线程上运行。但是,在使用此模式时必须格外小心——代码中出现的任何异常都可能会解开bitcoinj堆栈并导致对等断开连接,同样,重新进入库可能会导致锁定反转或其他问题。通常你应该避免这样做,除非你真的需要额外的表现,并确切知道你在做什么。
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
对象。 confidence
的概念体现了比特币是一个全球共识系统这一事实,该系统不断努力就全球交易顺序达成一致。因为这是一个难题(当遇到恶意行为者时),交易可能会被双倍花费(在比特币术语中我们说它已经 dead
)。也就是说,我们有可能相信我们已经收到了钱,后来我们发现世界其他地方不同意我们的看法。
Confidence
对象包含我们可以用来做出基于风险的决策的数据,这些决策是关于我们实际收到钱的可能性。它们还可以帮助我们在信心变化或达到某个阈值时学习。
Futures
是并发编程中的一个重要概念,bitcoinj大量使用它们,特别是我们将Guava扩展用于标准的Java Future类,称为 ListenableFuture
。 ListenableFuture
表示未来某种计算或状态的结果。你可以等待它完成(阻止调用线程),或者注册将被调用的回调。期货也可能会失败,在这种情况下,你会收到异常而不是结果。
在这里,我们要求 depth future
。当交易被链中的至少那么多块掩埋时,这个 future
就完成了。深度为1表示它出现在链中的顶部块中。所以在这里,我们说“当交易至少有一个确认时运行此代码”。通常你会使用一个名为 Futures.addCallback
的实用工具方法,虽然还有另一种注册监听器的方法,可以在下面的代码片段中看到。
然后,当发送给我们钱的交易确认时,我们只调用一个我们自己定义的方法叫做 forwardCoins
。
这里有一件重要的事情需要注意。 depth future
可能会运行,然后交易的 depth
变为小于 future
的参数。这是因为在任何时候比特币网络都可能经历“重组”,其中最着名的链从一个切换到另一个。如果你的交易出现在新链中的其他位置,则 depth
实际上可能会下降而不是向上。处理入库付款时,你应确保如果交易信心下降,你会尝试中止你为该资金提供的任何服务。你可以通过阅读 SPV安全模型
了解有关此主题的更多信息。
处理re-orgs和double spends是一个复杂的主题,本教程未涉及。你可以通过阅读其他文章了解更多信息。
ForwardingService
的最后一部分是发送我们刚刚收到的比特币。
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
方法。它需要三个参数: TransactionBroadcaster
(通常是 PeerGroup
),发送比特币的地址(这里我们使用我们之前从命令行解析的地址)以及要发送多少钱。
sendCoins
返回一个 SendResult
对象,该对象包含已创建的交易和一个 ListenableFuture
,可用于查明网络何时接受付款。如果钱包没有足够的钱, sendCoins
方法将抛出一个异常,其中包含一些关于缺少多少钱的信息。
比特币交易可以附加费用。这对于反拒绝服务机制很有用,但它主要是为了在通货膨胀率下降时激励系统后期的采矿。你可以通过自定义发送请求来控制附加到交易的费用:
SendRequest req = SendRequest.to(address, value); req.feePerKb = Coin.parseCoin("0.0005"); Wallet.SendResult result = wallet.sendCoins(peerGroup, req); Transaction createdTx = result.tx;
请注意,在这里,我们实际上设置了每千字节创建的交易的费用。这就是比特币的工作原理——交易的优先级由费用除以大小决定,因此较大的交易要求较高的费用被视为与较小的交易“相同”。
bitcoinj还有许多其他功能,本教程不涉及这些功能。你可以阅读其他文章以了解有关完整验证,钱包加密等的更多信息,当然JavaDocs还详细介绍了完整的API。
我建议你浏览我们的区块链教程和区块链技术博客,深入了解区块链,比特币,加密货币,以太坊,和智能合约。
汇智网原创翻译,转载请标明出处。这里是 原文