Try   HackMD

#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.gradledependenciesセクションに依存関係に追加します

implementation files('libs/bitcoinj-core-0.14-SNAPSHOT-bundled.jar')

また、bitcoinjのログを出力するためにslf4f-simpleも導入してください

impolementation 'org.slf4j:slf4j-simple:1.7.25'

Hello, World!

プロジェクトルートのsrc/main/kotlin内に.ktのファイルを作成し、その中にmain関数を作成してください

fun main(args Array<String>) {
   // ここに書いていく
}

このハンズオンではここにプログラムを記述していきます

Hello, Bitcoin!

セットアップ

// 利用するビットコインのネットワーク
// 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の各情報を取得することができるようになります

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

次のセクションに進む前に、取得したアドレスにいくらかコインを送金しておきましょう

TestNetのコインはBitcoin DevelopersのSlackにて /faucet コマンドを使って入手できます

Tips

プログラムが終了するのを防ぐには、もろもろの処理が終わった後に sleep するのが良いでしょう

Thread.sleep(Long.MAX_VALUE)

Tips 2

アドレスに送金しても残高が増えない場合は、しばらくプログラムを立ち上げた状態(sleep)にして待ってみましょう

送金してみよう!

実際にコインを送金してみましょう

// 所持している未承認のコインも利用可能にする
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倍のピアと接続している必要があり、それをなかなか満たさない場合があります
その場合は、デフォルトの最大接続数を以下のようにして減らしましょう

val kit = object : WalletAppKit(params, dir, filePrefix) {
    override fun createPeerGroup(): PeerGroup {
        return super.createPeerGroup().apply {
            maxConnections = 8
        }
    }
}

現在の接続状況については標準出力にログが出力されるためそちらを参照してみてください

Hello, Bitcoin Script!

先程の送金スクリプト(P2PKH)を自分で書いてみましょう

// 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にはそのようなスクリプトを生成するメソッドがあります

val script = ScriptBuilder.createOpReturnScript("Hello, Blockchain!".toByteArray())

これを用いて先程のようトランザクションを作り、実際にブロードキャストしてみましょう
送金額は0で構いません(ただし、送信手数料は取られます)

エクスプローラを用いて書き込んだデータを確認することができます
BLOCKTRAIL | Bitcoin API and Block Explorer
https://www.blocktrail.com/tBCC/tx/${tx.hashAsString}

また、自分でスクリプトを書くこともできます

val script = ScriptBuilder()
        .op(OP_RETURN)
        .data("Hello, Blockchain!".toByteArray())
        .build()

更に、OP_RETURNのデータを取得する場合は、以下のようになります

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つの公開鍵が必要です

// インポートされた鍵がなければ新しく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つの署名が必要です

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時間経過していないと、そのアドレス宛のコインを使うことができないというアドレスを作ってみましょう

一つ前の章で説明したことは省いています

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アドレスに送られたコインを利用してみましょう

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
bitcoincash.org/may-2018-reenabled-opcodes.md at master · bitcoincashorg/bitcoincash.org · GitHub

Android Sample App

Kotlinを用いたbitcoinj.cashのAndroidサンプルアプリを作りました!
GitHub - yenom/bitcoinj.cash-Sample-App: bitcoinj.cash Sample Android App

MITライセンスの範囲内でご自由にお使いください

Androidにおける注意点 (重要)

bitcoinjをAndroidで使うために、build.gradleのandroidセクションに以下の記述を追記する必要があります
これを追記しなければビルドに失敗してしまいます(罠)

packagingOptions {
    exclude 'javax/annotation/CheckReturnValue.java'
    exclude 'lib/x86_64/darwin/libscrypt.dylib'
}