chike0905の日記

何者かになりたい

btcdで独自ネットワークを構築する

概要

BitcoinのGo言語実装としてbtcdがある。Bitcoinの様々な研究開発を行う際に、実際のmainnetとは異なり価値の持たない通貨を扱う別のネットワークであるtestnetがよく用いられる。しかし、testnet上では、さまざまな試験が世界中で行われており、TXを投入しても承認に時間がかかるなど、様々な試験をするには適さない面がある。本項では、btcdを拡張し、testnet相当の別のネットワークを構築する手順を記す。

環境

  • btcd v0.20.1-beta (commit id: f3ec13030e4e828869954472cbc51ac36bee5c1d)
    • 以下のコマンドで$GOPATHへダウンロードし、$GOPATH/src/github.com/btcsuite/btcd以下のソースを改修していく go get -d github.com/btcsuite/btcd
  • btcwallet (commit id:704cd189ac2386b54ab64e17f67f1c7c5ef5c7ac)
    • btcd上で送金などの操作を行うために利用
    • btcd同様に以下のコマンドでダウンロードし、改修を行う go get -d github.com/btcsuite/btcwallet

Btcdの改修

Btcdで書き換えるべきファイル

  • chaincfg/genesis.go: Genesis Blockの設定パラメータ
  • chaincfg/params.go : ネットワーク全体に関わるパラメータ
  • config.go: 上記chaincfg/params.goを読み込むプログラム
  • params.go: config.goからchaincfg/params.goを読み込むためのインターフェース
  • wire/protocol.go: ノード通信時のメッセージプレフィックスなどの設定

パラメータ読み込み部の改修

btcdでは、btcd.go内でloadConfig()を呼ぶことで、ノードの設定を読み込んでいる。loadConfig()の実態はconfig.goの中にある。まずは、type config structの中のコマンドラインパラメータの中に、mynetのオプションを追加する。

type config struct {
...
    TestNet3             bool          `long:"testnet" description:"Use the test network"`
+    MyNet               bool          `long:"mynet" description:"Use the my network"`
    RegressionTest       bool          `long:"regtest" description:"Use the regression test network"`
    SimNet               bool          `long:"simnet" description:"Use the simulation test network"`
...

次にloadConfig()内のネットワークパラメータ読み込み部に追記する。

func loadConfig() (*config, []string, error) {
...
        if cfg.TestNet3 {
                numNets++
                activeNetParams = &testNet3Params
        }
        if cfg.RegressionTest {
                numNets++
                activeNetParams = &regressionNetParams
        }
+        if cfg.MyNet {
+                numNets++
+                activeNetParams = &myNetParams
+        }
        if cfg.SimNet {
                numNets++
                // Also disable dns seeding on the simulation test network.
                activeNetParams = &simNetParams
                cfg.DisableDNSSeed = true
        }
...

ここで呼ばれる*NetParamsは、params.go内で定義されるため、追記する。ここでRPCのためのポートを任意に設定する。

...
// MyNetParams
var MyNetParams = params{
        Params:  &chaincfg.MyNetParams,
        rpcPort: "11454",
}
...

通信時のprefix設定

ノードは通信時に、ネットワークを識別するためのprefixをつける。それらは、wire/protocol.goに定義されている。このprefixはどのように定義されるべきかは筆者は把握していない。ここでは任意に設定した。

// BitcoinNet represents which bitcoin network a message belongs to.
type BitcoinNet uint32

// Constants used to indicate the message bitcoin network.  They can also be
// used to seek to the next message when a stream's state is unknown, but
// this package does not provide that functionality since it's generally a
// better idea to simply disconnect clients that are misbehaving over TCP.
const (
        // MainNet represents the main bitcoin network.
        MainNet BitcoinNet = 0xd9b4bef9

        // TestNet represents the regression test network.
        TestNet BitcoinNet = 0xdab5bffa

        // TestNet3 represents the test network (version 3).
        TestNet3 BitcoinNet = 0x0709110b

        // MyNet represents the my network.
        MyNet BitcoinNet = 0x79616a75              

        // SimNet represents the simulation test network.
        SimNet BitcoinNet = 0x12141c16
)

設定パラメータの作成

上記で読み込むよう設定したchaincfg.MyNetParamschaincfg/params.goの中で定義される。今回はtestnetとregtestの設定をベースに以下の設定を追記した。ここではここで記される内容の詳細には立ち入らないが、ここのパラメータを色々いじることで様々なネットワークを作ることができる。

...
// MyNet
var MyNetParams = Params{
    Name:        "mynet",
    Net:         wire.MyNet,
    DefaultPort: "11451",
    DNSSeeds: nil,

    // Chain parameters
    GenesisBlock:             &MyNetGenesisBlock,
    GenesisHash:              &MyNetGenesisHash,
    PowLimit:                 regressionPowLimit,
    PowLimitBits:             0x207fffff, // same as regtest
    BIP0034Height:            0, // always
    BIP0065Height:            0, // always
    BIP0066Height:            0, // always
    CoinbaseMaturity:         100,
    SubsidyReductionInterval: 210000,
    TargetTimespan:           time.Hour * 24 * 14, // 14 days
    TargetTimePerBlock:       time.Minute * 10,    // 10 minutes
    RetargetAdjustmentFactor: 4,                   // 25% less, 400% more
    ReduceMinDifficulty:      true,
    MinDiffReductionTime:     time.Minute * 20, // TargetTimePerBlock * 2
    GenerateSupported:        true,

    // Checkpoints ordered from oldest to newest.
    Checkpoints: nil,

    // Consensus rule change deployments.
    //
    // The miner confirmation window is defined as:
    //   target proof of work timespan / target proof of work spacing
    RuleChangeActivationThreshold: 1512, // 75% of MinerConfirmationWindow
    MinerConfirmationWindow:       2016,
    Deployments: [DefinedDeployments]ConsensusDeployment{
        DeploymentTestDummy: {
            BitNumber:  28,
            StartTime:  0, // always
            ExpireTime: 0, // always
        },
        DeploymentCSV: {
            BitNumber:  0,
            StartTime:  0, // always
            ExpireTime: 0, // always
        },
        DeploymentSegwit: {
            BitNumber:  1,
            StartTime:  0, // always
            ExpireTime: 0, // always
        },
    },

    // Mempool parameters
    RelayNonStdTxs: true,

    // Human-readable part for Bech32 encoded segwit addresses, as defined in
    // BIP 173.
    Bech32HRPSegwit: "tb", // always tb for test net

    // Address encoding magics
    PubKeyHashAddrID:        0x00, // starts with 1
    ScriptHashAddrID:        0xc4, // starts with 2
    WitnessPubKeyHashAddrID: 0x03, // starts with QW
    WitnessScriptHashAddrID: 0x28, // starts with T7n
    PrivateKeyID:            0xef, // starts with 9 (uncompressed) or c (compressed)

    // BIP32 hierarchical deterministic extended key magics
    HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv
    HDPublicKeyID:  [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub

    // BIP44 coin type used in the hierarchical deterministic path for
    // address generation.
    HDCoinType: 1,
}
...

Genesis Blockのパラメータ設定

chaincfg.MyNetParamsの中からGenesis Blockのデータと、そのハッシュをchaincfg/genesis.goから読み込んでいる。そこでGenesisハッシュ値などを設定する必要がある。そのため、Genesis Blockの生成は既存のスクリプトを用いて行った。しかしこのスクリプトはPython2.7でしか動作しない。 https://github.com/lhartikk/GenesisH0

以下のスクリプトで生成する。今回は、簡易的に実装するためにregtestと同様の値(ごく簡単なdifficultyでの生成)を行っている。

$ python genesis.py -n 00000000 -b 545259519 
04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73
algorithm: SHA256
merkle hash: 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b
pszTimestamp: The Times 03/Jan/2009 Chancellor on brink of second bailout for banks
pubkey: 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f
time: 1581663396
bits: 0x207fffff
Searching for genesis hash..
genesis hash found!
nonce: 1
genesis hash: 718790f769dc86320b52d4c176a94e775947e922dd019e99b91cc2f9ddbc48b8

上記スクリプトで生成した値をそれぞれのフィールドへ適用させる。ここでmyNetGenesisHashの値はリトルエンディアンになっていることに注意する。

...
var myNetGenesisHash = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy.
    0xb8, 0x48, 0xbc, 0xdd, 0xf9, 0xc2, 0x1c, 0xb9,
    0x99, 0x9e, 0x01, 0xdd, 0x22, 0xe9, 0x47, 0x59,
    0x77, 0x4e, 0xa9, 0x76, 0xc1, 0xd4, 0x52, 0x0b,
    0x32, 0x86, 0xdc, 0x69, 0xf7, 0x90, 0x87, 0x71,
})

// myNetMerkleRoot is the hash of the first transaction in the genesis
// block for the my network.  It is the same as the merkle root
// for the main network.
var myNetGenesisMerkleRoot = genesisMerkleRoot

// myNetGenesisBlock defines the genesis block of the block chain which
// serves as the public transaction ledger for the my network.
var myNetGenesisBlock = wire.MsgBlock{
    Header: wire.BlockHeader{
        Version:    1,
        PrevBlock:  chainhash.Hash{},          // 0000000000000000000000000000000000000000000000000000000000000000
        MerkleRoot: myNetGenesisMerkleRoot, // 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b
        Timestamp:  time.Unix(1581663396, 0),
        Bits:       0x207fffff,               // 545259519 [7fffff0000000000000000000000000000000000000000000000000000000000]
        Nonce:      1,
    },
    Transactions: []*wire.MsgTx{&genesisCoinbaseTx},
}
...

ビルドと起動確認

以下のコマンドでビルドする。$GOPATH/binにバイナリが吐き出される。

GO111MODULE=on go install -v . ./cmd/...

以下のコマンドでbtcdが起動する。設定したGenesisで正常に2回起動されれば成功。というのも、初回の起動の時にgenesisで設定したものがブロックとしてストレージに書き込まれ、2回目の起動の時にのみGenesisHashとの照合が行われるため。もし2回目が起動しなければ、Genesis設定周りのパラメータを再確認する。

$GOPATH/bin/btcd --mynet

btcctlを用いたインタラクション

btcdは同梱されているbtctlを用いてインタラクションを行う。btcctl自体にmynetオプションを追加しても良いが、ここではconfigファイルを用いた設定方法を記す。

まずは、btcdとbtcctlの設定ファイルを以下のように記述し、それぞれbtcd.confbtcctl.confとして保存する。

[Application Options]
rpcuser=myuser
rpcpass=hogehoge

btcdを以下のコマンドで起動する。

$GOPATH/bin/btcd --mynet -C $PATH_TO_btcd.conf

別のコンソールで、以下のコマンドでインタラクションできることを確認する。ここではparams.goで設定したRPC Portを指定するのを忘れてはならない。

$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11454 getblockchaininfo

マイニングの実施と確認

今回設定しているchaincfg/params.goの中で、btcdで作ったネットワーク上でregtestと同じようにgenreate(ブロックの生成)が簡単にできるようになっている。そこでブロックの生成を実施し、送金を試してみる。

btcwalletと改修後のビルド注意点

btcdからは送金などにまつわるいわゆるwalletの機能は除外されている。walletの機能はbtcwalletという実装に含まれている。このbtcwalletは、RPCでbtcdと通信し、walletの機能を提供する。したがって、go実装のものを使ってbitcoinの操作を行うためには、btcctlをインターフェースとして、nodeへの操作時はbtcdへ、walletの操作時はbtcwalletへRPCでコマンドを送信し操作する事になる。

ここでは、btcwalletを以下のコマンドでダウンロードし、mynetオプションを利用できるように改修していく。

go get -d github.com/btcsuite/btcwallet

改修したbtcwalletをビルドする際は以下のコマンド。この時、公式のドキュメントではGO111MODULE=on環境変数を設定するが、btcdの中のchaincfgをモジュールとして読み込んでいる。そのため、改修したローカルのbtcdを参照するようにGO111MODULE=offとしなければならないことに注意する。

GO111MODULE=off go install -v . ./cmd/...

btcwalletの改修

基本的には、コマンドラインmynetオプションを追加し、それが指定された時は、指定したchaincfgのパラメータを読みにいく、という基本的な流れは変わらない。以下にdiffを記す。

+++ b/config.go
@@ -53,6 +53,7 @@ type config struct {
        CreateTemp    bool                    `long:"createtemp" description:"Create a temporary simulation wallet (pass=password) in the data directory indicated; must call with --datadir"`
        AppDataDir    *cfgutil.ExplicitString `short:"A" long:"appdata" description:"Application data directory for wallet config, databases and logs"`
        TestNet3      bool                    `long:"testnet" description:"Use the test Bitcoin network (version 3) (default mainnet)"`
+       MyNet         bool                    `long:"mynet" description:"Use the my Bitcoin network (default mainnet)"`
        SimNet        bool                    `long:"simnet" description:"Use the simulation test network (default mainnet)"`
        NoInitialLoad bool                    `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"`
        DebugLevel    string                  `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
@@ -365,6 +366,10 @@ func loadConfig() (*config, []string, error) {
                activeNet = &netparams.SimNetParams
                numNets++
        }
+       if cfg.MyNet {
+               activeNet = &netparams.MyNetParams
+               numNets++
+       }
        if numNets > 1 {
                str := "%s: The testnet and simnet params can't be used " +
                        "together -- choose one"
diff --git a/netparams/params.go b/netparams/params.go
index c15047e..aeb5a57 100644
--- a/netparams/params.go
+++ b/netparams/params.go
@@ -37,3 +37,10 @@ var SimNetParams = Params{
        RPCClientPort: "18556",
        RPCServerPort: "18554",
 }
+
+// Mynet
+var MyNetParams = Params{
+       Params:        &chaincfg.MyNetParams,
+       RPCClientPort: "11454",
+       RPCServerPort: "11455",
+}

BtcdとBtcwalletのセットアップ

まずはbtcwalletでwalletを作成する。

$GOPATH/bin/btcwallet --mynet --create

次に、btcdを起動するが、デフォルトではwallet<->btcdとwallet<->btcctlの通信はTLSを用いる。今回は、簡易的に試験するために、それらを外すオプションをつける。

$GOPATH/bin/btcd --mynet -C $PATH_TO_btcd.conf --notls

次に、btcwalletを起動し、btcdへ接続する。このとき,-uオプション(ユーザ名)と-Pオプション(パスワード)は、btcd.confで設定したものを入力する。btcd同様にTLSを無効化するオプションを入れている。

$GOPATH/bin/btcwallet -u myuser -P hogehoge --mynet --noclienttls --noservertls

btcctlから、btcd、btcwalletにそれぞれgetblockchaininfoコマンドを送信し、それぞれ同じ出力が得られれば改修は成功。

# to btcd
$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11454 --notls getblockchaininfo
# to btcwallet
$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11455 --notls getblockchaininfo

coinbaseアカウントの設定

btcwalletではcreate時にdefaultアカウントが作成されている。以下のコマンドでアカウントのアドレスが確認できる。

$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11455 getaccountaddress default

出力されたアドレスを、btcd.confへcoinbaseアカウントとして設定し、再起動する。

[Application Options]
rpcuser=myuser
rpcpass=hogehoge
+ miningaddr=$DUMPED_DEFAULT_ACCOUNT

generateコマンドの実行と残高確認

bitcoinでは、マイニング報酬は100ブロック以降でないと送金できない。そのため、101ブロックマイニングをgenerateコマンドで実行する。

$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11454 generate 101

btcwallet側のログに、同期したメッセージが表示されれば以下のコマンドで残高が増えているのが確認できる。同期が起こらなければ、btcwalletを再起動すると起動時にbtcdとブロックの同期が行われる。

$GOPATH/bin/btcctl -C $PATH_TO_btcctl.conf -s localhost:11455 getbalance default

dockerで複数ノードを起動

適当に書いた以下のDockerfileで本実装を複数ノードで動かしてみる。

FROM golang:1.13.4-buster
LABEL maintainer="Rysouke Abe <chike@sfc.wide.ad.jp>"

RUN apt update 
RUN apt install -y vim

RUN go get -d github.com/btcsuite/btcd
RUN rm -r /go/src/github.com/btcsuite/btcd
RUN git clone https://github.com/chike0905/btcd.git /go/src/github.com/btcsuite/btcd
WORKDIR /go/src/github.com/btcsuite/btcd
RUN git checkout mynet
RUN go install -v . ./cmd/...

WORKDIR /go
RUN go get -d github.com/btcsuite/btcwallet
RUN rm -r /go/src/github.com/btcsuite/btcwallet
RUN git clone https://github.com/chike0905/btcwallet.git /go/src/github.com/btcsuite/btcwallet
WORKDIR /go/src/github.com/btcsuite/btcwallet
RUN git checkout mynet
RUN GO111MODULE=off go install -v . ./cmd/...

WORKDIR /root/.btcd
ADD elements/btcd.conf /root/.btcd/btcd.conf
WORKDIR /root/.btcwallet
ADD elements/btcctl.conf /root/.btcwallet/btcctl.conf

WORKDIR /root
CMD ["bash"]

btcdmynet:latestとしてビルドする。

docker build -t btcdmynet:latest .

HVのコンテナ同士でのみ通信できるブリッジネットワークを作成し、dockerコンテナをそれぞれそれらに接続する。

docker network create --driver=bridge --internal internalnet

dockerコンテナをそれぞれ起動。

docker run -it --rm --net internalnet --name btcd1 btcdmynet:latest 
docker run -it --rm --net internalnet --name btcd2 btcdmynet:latest 

上記手順でそれぞれのコンテナ上でbtcdを起動させてから、btc1からbtcctlのaddnodeコマンドでpeerを追加する

btcctl -C .btcwallet/btcctl.conf -s localhost:11454 --notls addnode $ADDR_BTC2 add

btc1上で上記手順を用いてgenerateコマンドを走らせると、btc2上に同期されていることがわかる。

まとめ

本稿では、testnet相当の別のネットワークを柔軟に設定するために、btcd/btcwalletを改修して独自のネットワークを構築する手順を記した。本項では、chaincfg/params.goの中のパラメータの詳細には立ち入らなかったが、それらをいじることで様々なパラメータを持つネットワークを構築することが可能である。PoW周りのパラメータなど、様々な値を操作することが可能であり、様々な実験が実施可能であることが期待できる。

筆者の感想にはなるが、btcdはbitcoindに比べて後発なのもあり、そのアーキテクチャなどが洗練されつつあるイメージがある。本稿で述べたような手法を用いながら、様々な検証を行い、さらなる洗練が期待される。