论坛首页 Java企业应用论坛

java开发比特币类库bitcoinj入门指南

浏览 6301 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2018-10-08   最后修改:2018-10-19

bitcoinj是一个使用比特币协议的库。它可以维护钱包,发送/接收交易而无需比特币核心的本地副本,并具有许多其他高级功能。它是用Java实现的,但可以通过任何JVM兼容语言中使用:包括Python和JavaScript中的示例。

它附带完整的文档,并建立了许多大型,众所周知的比特币应用程序和服务。下面我们来看看如何使用它。

初始设置

bitcoinj内置了日志记录和断言。无论是否指定了-ea标志,都会默认检查断言。记录由SLF4J库处理。它允许你选择你更喜欢使用的日志系统,例如JDK日志记录,Android日志记录等。默认情况下,我们使用简单的logger来打印stderr感兴趣的大部分内容。你可以通过切换lib目录中的jar文件来选择一个新的logger。

bitcoinj使用Maven作为其构建系统,并通过git分发。你可以使用源代码/ jar下载,但直接从源存储库获取它更安全。

要获取代码并安装它,请抓取MavenGradle,并将其添加到你的路径中。还要确保安装了git。可能你的Java IDE也有一些Maven/Gradle和Git集成,但是通过命令行使用它们还是非常有用。

现在获取最新版本的代码。你可以使用使用Maven使用Gradle页面上的说明——只需在那里运行命令,你就可以获得正确的代码版本(除非此网站本身已被泄露)。这是为了防止受损镜像或源代码下载——因为git使用源树哈希工作,如果以正确的方式获得源哈希,则可以保证最终得到正确的代码。

你可以在这里阅读完整的程序。

基本结构

bitcoinj应用程序使用以下对象:

  • NetworkParameters实例,用于选择你所在的网络(生产或测试)。
  • 用于存储ECKeys和其他数据的Wallet实例。
  • 用于管理网络连接的PeerGroup实例。
  • 一个BlockChain实例,它管理共享的全局数据结构,使比特币工作。
  • 一个BlockStore实例,它将块链数据结构保存在某个位置,就像在磁盘上一样。
  • WalletEventListener实现,用于接收钱包交易。

为了简化设置,还有一个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";
}

有多个独立的,独立的比特币网络:

  • 人们买卖东西的主要或“生产”网络。
  • 公共测试网络(testnet)不时被重置并存在以供我们使用新功能。
  • 回归测试模式,它不是公共网络,需要你自己运行带有-regtest标志的比特币守护进程。

每个网络都有自己的创世块,自己的端口号和自己的地址前缀字节,以防止你不小心尝试通过网络发送比特币(这将无法正常工作)。这些事实被封装到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由各种层组成,每层都在比最后一层更低的层次上运行。想要发送和接收资金的典型应用程序至少需要BlockChainBlockStorePeerGroupWallet。所有这些对象需要相互连接,以便数据正确流动。阅读如何融合在一起,了解有关数据如何通过基于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主线程中。但是,事件侦听器本身不需要是线程安全的,因为交易将按顺序排队并执行。你也不必担心使用多线程库时通常会出现的许多其他问题(例如,重新进入库是安全的,并且可以安全地执行阻塞操作)。

关于编写GUI应用程序的说明

大多数小工具工具包(如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);11

现在,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类,称为ListenableFutureListenableFuture表示未来某种计算或状态的结果。你可以等待它完成(阻止调用线程),或者注册将被调用的回调。期货也可能会失败,在这种情况下,你会收到异常而不是结果。

在这里,我们要求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。

我建议你浏览我们的区块链教程和区块链技术博客,深入了解区块链,比特币,加密货币,以太坊,和智能合约。

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • php比特币开发教程本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。

这里是原文 

论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics