# #BitcoinHackathon #2 Hands-on for Android Dev. このハンズオンでは基本的なbitcoinjの使い方、コントラクトを書き方等を習得することができ、 ビットコイン初心者でもビットコインで遊ぶことができるようになります ## 下準備 ### プロジェクトの作成 ハンズオン用のプロジェクトをIntelliJ IDEA等のIDEで作成してください(Android Studioではありません) このハンズオンでは、Kotlin, Gradle, IntelliJ IDEA、bitcoinj.cash(BCH)を用いますが、 Javaや他のIDE、bitcoinj(BTC)を用いても構いません 但し、簡略化のため差分の解説は行いません ### bitcoinj.cashの導入 bitcoinj.cashとはBitcoin ProtocolのJava実装ライブラリであるbitcoinj(BTC)のBCH対応版です Maven Repositoryで配布されているbitcoinj.cashはバージョンが古いため、最新版をコンパイルしました https://github.com/yenom/bitcoinj.cash-Sample-App/blob/master/app/libs/bitcoinj-core-0.14-SNAPSHOT-bundled.jar プロジェクトルートの`libs`ディレクトリ直下(無ければ作ってください)にダウンロードしたjarファイルを置き、`build.gradle`の`dependencies`セクションに依存関係に追加します ```Groovy implementation files('libs/bitcoinj-core-0.14-SNAPSHOT-bundled.jar') ``` また、bitcoinjのログを出力するために`slf4f-simple`も導入してください ```Groovy impolementation 'org.slf4j:slf4j-simple:1.7.25' ``` ### Hello, World! プロジェクトルートの`src/main/kotlin`内に`.kt`のファイルを作成し、その中にmain関数を作成してください ``` fun main(args Array<String>) { // ここに書いていく } ``` このハンズオンではここにプログラムを記述していきます ## Hello, Bitcoin! ### セットアップ ```Kotlin // 利用するビットコインのネットワーク // MainNetを利用するにはMainNetParams()を用いる val params = TestNet3Params() // Bitcoinの各種データ(秘密鍵含む)を保存するディレクトリ // Androidの場合は context.filesDir などを利用するとよい val dir = File(".") // 各データのファイル名のPrefix val filePrefix = "testnet" // WalletAppKitはBitcoinを用いたアプリを作るときに便利なUtilクラス val kit = WalletAppKit(params, dir, filePrefix) // Kitのセットアップを開始する kit.startAsync() // Kitのセットアップが終わるまで待つ kit.awaitRunning() ``` ### Walletの情報を取得する `kit.awaitRunning()` が終了した後に、Walletの各情報を取得することができるようになります ```Kotlin val wallet = kit.wallet() // 残高 // BalanceType.ESTIMATED は承認前のトランザクションも残高計算に含めるというオプション val balance = wallet.getBalance(Wallet.BalanceType.ESTIMATED) // 現在の受取用アドレス val address = CashAddressFactory.create().getFromBase58(params, wallet.currentReceiveAddress().toBase58()) // このWalletに関するトランザクションのリストを時間順で取得 val transactions = wallet.transactionsByTime // コインを受け取ったときに何か処理する場合に用いる wallet.addCoinsReceivedEventListener { wallet, tx, prevBalance, newBalance -> // something... } ``` 詳細なAPIに関しては以下のドキュメントを参照してください [cash.bitcoinj:bitcoinj-core:0.14.5.2 API Doc :: Javadoc.IO](http://www.javadoc.io/doc/cash.bitcoinj/bitcoinj-core/0.14.5.2) 次のセクションに進む前に、取得したアドレスにいくらかコインを送金しておきましょう TestNetのコインはBitcoin DevelopersのSlackにて `/faucet` コマンドを使って入手できます #### Tips プログラムが終了するのを防ぐには、もろもろの処理が終わった後に `sleep` するのが良いでしょう ```Kotlin Thread.sleep(Long.MAX_VALUE) ``` #### Tips 2 アドレスに送金しても残高が増えない場合は、しばらくプログラムを立ち上げた状態(`sleep`)にして待ってみましょう ### 送金してみよう! 実際にコインを送金してみましょう ```Kotlin // 所持している未承認のコインも利用可能にする kit.wallet().allowSpendingUnconfirmedTransactions() // 送金先アドレス val to = CashAddressFactory.create().getFromFormattedAddress(params, "bchtest:...") // 0.005 BCH 送る val sendAmount = Coin.parseCoin("0.005") // このメソッドはトランザクションの作成、署名、ブロードキャスト等を隠蔽している wallet.sendCoins( kit.peerGroup(), to, sendAmount, true ) ``` bitcoinjはSPVライブラリであるため、自身がBitcoin Networkの参加者になります つまり、ブロードキャスト等にREST APIなどは用いてないため、送金速度は自身の接続状況等に依存することに注意してください #### Tips 3 bitcoinjに於いて、トランザクションのブロードキャストには最大接続数の0.8倍のピアと接続している必要があり、それをなかなか満たさない場合があります その場合は、デフォルトの最大接続数を以下のようにして減らしましょう ```Kotlin val kit = object : WalletAppKit(params, dir, filePrefix) { override fun createPeerGroup(): PeerGroup { return super.createPeerGroup().apply { maxConnections = 8 } } } ``` 現在の接続状況については標準出力にログが出力されるためそちらを参照してみてください ## Hello, Bitcoin Script! 先程の送金スクリプト(P2PKH)を自分で書いてみましょう ```Kotlin // P2PKHのスクリプト // ScriptBuilderを用いることで簡単にスクリプトを書くことができる val script = ScriptBuilder() .op(OP_DUP) .op(OP_HASH160) .data(to.hash160) .op(OP_EQUALVERIFY) .op(OP_CHECKSIG) .build() val tx = Transaction(params).apply { addOutput(sendAmount, script) } val req = SendRequest.forTx(tx).apply { // BCHのために必要 useForkId = true } // このメソッドがトランザクションのInputをよしなに選択して、署名してくれる wallet.completeTx(req) // トランザクションをブロードキャストする wallet.commitTx(req.tx) ``` ## ブロックチェーンに任意のデータを刻もう `OP_RETURN`を用いると任意のデータをブロックチェーンに刻むことができます bitcoinjにはそのようなスクリプトを生成するメソッドがあります ```Kotlin val script = ScriptBuilder.createOpReturnScript("Hello, Blockchain!".toByteArray()) ``` これを用いて先程のようトランザクションを作り、実際にブロードキャストしてみましょう 送金額は0で構いません(ただし、送信手数料は取られます) エクスプローラを用いて書き込んだデータを確認することができます [BLOCKTRAIL | Bitcoin API and Block Explorer](https://www.blocktrail.com/tBCC/tx/b949641b43bbce77e66dea320a813cf0280b56a4f7df788783ed8e4ac9902063) `https://www.blocktrail.com/tBCC/tx/${tx.hashAsString}` また、自分でスクリプトを書くこともできます ```Kotlin val script = ScriptBuilder() .op(OP_RETURN) .data("Hello, Blockchain!".toByteArray()) .build() ``` 更に、`OP_RETURN`のデータを取得する場合は、以下のようになります ```Kotlin val txOut = wallet.getTransaction(Sha256Hash.wrap(tx.hashAsString))!!.getOutput(0) val data = txOut.scriptPubKey.chunks[1] ``` 但し、bitcoinjはSPVライブラリであるため自分のウォレットに関するトランザクション(自分が送信したor受信した)しか取得できません 任意のトランザクションのデータを処理したい場合は、https://rest.bitcoin.com/ などのREST APIを用いるのが良いでしょう ## Play with Multisig and P2SH! 2つの秘密鍵による署名がないとコインが使えないというアドレス(2 of 2のMultisigアドレス)を作成して、実際に署名してみましょう ### Multisig(P2SH)アドレスを作る 2 of 2のMultisigアドレスを作成してみましょう もちろん作成には2つの公開鍵が必要です ```Kotlin // インポートされた鍵がなければ新しくMultisig用の鍵を作る // この鍵は管理しやすいようにHD Walletを使って取得/管理しても良い // 鍵を作ったらWalletにインポートしておく val myKey = if (wallet.importedKeys.isEmpty()) ECKey().also { wallet.importKey(it) } else wallet.importedKeys[0] // このアドレスで用いるもう一つの公開鍵 // 今回は便宜上、同じウォレット内で行う // 通常は、他のウォレットで作成した公開鍵のバイト配列(ECKey#getPubKey())を用いて取得してくる val partnerKey = if (wallet.importedKeys.size < 2) ECKey().also { wallet.importKey(it) } else wallet.importedKeys[1] val keys = arrayListOf(myKey, partnerKey) // 今回は2 of 2のMultisigアドレスを作成したいため、引数に2を渡す // redeemScript.program のバイト列は、このアドレスに送られたコインを使うときに必要になるため // 保持しておく必要がある val redeemScript = ScriptBuilder.createRedeemScript(2, keys) val script = ScriptBuilder.createP2SHOutputScript(redeemScript) val multisigAddress = Address.fromP2SHScript(params, script) // 作成したP2SHアドレスを監視リスト(Bloom Filter)に加える // こうすることでこのアドレス宛のトランザクションを検知できるようになる wallet.addWatchedAddress(multisigAddress) ``` 2(TestNet)から始まるアドレスが作成されたはずです ちなみにMainnetは3から始まります このアドレス宛にいくらか送金してみましょう ### Multisigアドレスに送られたコインを署名して使う Multisigアドレスに送られたコインを使用してみましょう 今回は2 of 2のMultisigアドレスを作成したため、2つの署名が必要です ```Kotlin val to: Address = ... // 利用したいMultisigアドレス宛のUTXO // txHashはExplorerer等から取得してきてください val utxo = wallet.getTransaction(Sha256Hash.wrap(txHash)) val multisigOutput = utxo!!.getWalletOutputs(wallet).first() // 手数料として少し送金額を減らす // 本来ならばトランザクションのバイト数に応じて決めるのがよい // ex.) 1 satoshi per byte val sendAmount = multisigOutput.value.subtract(Coin.valueOf(1000)) val spendTx = Transaction(params).apply { addOutput(sendAmount, to) addInput(multisigOutput) } // トランザクションのハッシュを作成する val sigHash = spendTx.hashForSignatureWitness(0, redeemScript, multisigOutput.value, Transaction.SigHash.ALL, false) // トランザクションのハッシュ値に署名する val signature = myKey.sign(sigHash) // もう一つの鍵で署名する // 署名をエンコード/デコードするには // ECKey.ECDSASignature#encodeToDER, decodeFromDER を利用すると良い val partnerSignature = partnerKey.sign(sigHash) val sigs = arrayOf(partnerSignature, signature).map { TransactionSignature(it, Transaction.SigHash.ALL, false, true) }.toList() // 署名をinputに設定する spendTx.inputs.first().scriptSig = ScriptBuilder.createP2SHMultiSigInputScript(sigs, redeemScript) // 検証 // うまく署名等ができていないと例外が発生する spendTx.inputs.first().verify() // ブロードキャスト wallet.commitTx(spendTx) ``` このようにして、家族、会社などのメンバーで管理するアドレスを作ることができるようになります ## コイン with 時限 `OP_CHECKSIGVERIFY`を使うと、指定した日時を過ぎないと利用できないトランザクション(コイン)を作ることができます ### LockTimeアドレスを作る アドレス(P2SH)を作成した時刻から24時間経過していないと、そのアドレス宛のコインを使うことができないというアドレスを作ってみましょう 一つ前の章で説明したことは省いています ```Kotlin val key = if (wallet.importedKeys.size < 3) ECKey().also { wallet.importKey(it) } else wallet.importedKeys[2] val dateBytes = ByteArray(4).apply { val tomorrow = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) } // 指定する時刻をEpochにして、それをuint32としてリトルエンディアンのバイト列にする Utils.uint32ToByteArrayLE(tomorrow.timeInMillis / 1000, this, 0) } // このRedeem Scriptのデータ(Script#program)は保存しておく(再確認) val redeemScript = ScriptBuilder() .data(dateBytes) .op(OP_CHECKLOCKTIMEVERIFY) .op(OP_DROP) .data(key.pubKey) .op(OP_CHECKSIG) .build() val script = ScriptBuilder.createP2SHOutputScript(redeemScript) val locktimeAddress = Address.fromP2SHScript(params, script) ``` この `locktimeAddress` にいくらか送金してみましょう このアドレスに送金したコインは指定した時刻を過ぎないと利用できません ### LockTimeトランザクションを使う では、この作成したLockTimeアドレスに送られたコインを利用してみましょう ```Kotlin val to: Address = ... val utxo = wallet.getTransaction(Sha256Hash.wrap(...)) val redeemScript = Script(byteArrayOf(...)) // redeemScriptからアドレスに紐付けられた時限を求める val locktime = Utils.readUint32(redeemScript.chunks.first().data, 0) val locktimeOutput = utxo!!.getWalletOutputs(wallet).first() val sendAmount = locktimeOutput.value.subtract(Coin.valueOf(5000)) val spendTx = Transaction(params).apply { addInput(locktimeOutput) addOutput(sendAmount, to) // OP_CHECKSIGVERIFY の要件により、この2つの設定が必要となる inputs.first().sequenceNumber = 0 lockTime = locktime } val sigHash = spendTx.hashForSignatureWitness(0, redeemScript, locktimeOutput.value, Transaction.SigHash.ALL, false) // アドレスに紐付けられた秘密鍵を取得 val key = wallet.findKeyFromPubKey(redeemScript.chunks[3].data)!! // 署名 val signature = TransactionSignature(key.sign(sigHash), Transaction.SigHash.ALL, false, true).encodeToBitcoin() spendTx.inputs.first().scriptSig = ScriptBuilder().data(signature).data(redeemScript.program).build() // 検証 spendTx.inputs.first().verify() // ブロードキャスト wallet.commitTx(spendTx) ``` どうでしょうか このアドレスはアドレス作成時から1日経過していなければそのコインを利用することはできないため、broadcastに失敗したはずです このようにP2SHを用いると、様々なスクリプトを書くことができるようになります ここで解説したOP_CODEはほんの一部であるため(条件分岐もできる)、以下の資料等を参照してみてください [Script - Bitcoin Wiki](https://en.bitcoin.it/wiki/Script#Opcodes) [bitcoincash.org/may-2018-reenabled-opcodes.md at master · bitcoincashorg/bitcoincash.org · GitHub](https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/may-2018-reenabled-opcodes.md) ## Android Sample App Kotlinを用いたbitcoinj.cashのAndroidサンプルアプリを作りました! [GitHub - yenom/bitcoinj.cash-Sample-App: bitcoinj.cash Sample Android App](https://github.com/yenom/bitcoinj.cash-Sample-App) MITライセンスの範囲内でご自由にお使いください ### Androidにおける注意点 (重要) bitcoinjをAndroidで使うために、`build.gradle`のandroidセクションに以下の記述を追記する必要があります これを追記しなければビルドに失敗してしまいます(罠) ```Groovy packagingOptions { exclude 'javax/annotation/CheckReturnValue.java' exclude 'lib/x86_64/darwin/libscrypt.dylib' } ```