ビットコインソースコード研究ノート(I)

ビットコインソースコード研究ノート(I)

序文

ブロックチェーンの基盤となるコアテクノロジーを理解せずにブロックチェーン開発に取り組むだけでは不十分です。多くの人は、ビットコインのホワイトペーパーを読んでも、ビットコインがどのように実装されているかをまだ知りません。これは、ビットコインのソースコードが巧妙に設計されており、ホワイトペーパーには記載されていない設計が多数あるためです。さらに、ビットコイン自体に関するドキュメントも少なく、初心者にとっては理解するのが難しくなっています。ブロックチェーンを紹介する書籍や記事は数多くありますが、ソースコードから分析したものはほとんどありません。ブロックチェーンを半年勉強した後、ビットコインのソースコードに関するチュートリアルを書き始めました。このチュートリアルは理解しやすく、最も古典的なブロックチェーンである Bitcoin の C++ クライアント ソース コードを分析することで、開発者は最短時間でブロックチェーン テクノロジーを開始できます。ビットコインのソースコードを理解することで、開発者はブロックチェーンの仕組みをより深く理解し、実際の状況に基づいてアプリケーションを変更および調整できるようになります。

この記事で引用されているソースコードはすべて、ビットコイン クライアントのオリジナル バージョン、つまり Satoshi Nakamoto によって公開されたソースコードの最初のバージョンのものです。クライアントは約 16,000 行のコードで構成されています。 Bitcoin クライアントは長年にわたっていくつかの主要なアップデートが行われてきましたが、そのデータ構造と原則は開始以来変わっていません。この記事は、テキストの厳密さと正確さを確保するために最善を尽くします。表現上の省略は避けられないため、訂正は歓迎します。

第1章

このセクションでは、Bitcoin クライアントが Bitcoin アドレスを生成し、新しいトランザクションを作成する方法について説明します。

main.cpp にある GenerateNewKey() メソッドを見てみましょう。

 bool AddKey(const CKey& キー)
{
    CRITICAL_BLOCK(cs_mapKeys)
    {
        mapKeys[key.GetPubKey()] = key.GetPrivKey();
        mapPubKeys[Hash160(key.GetPubKey())] = key.GetPubKey();
    }
    CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()) を返します。
}
ベクトル<unsigned char> GenerateNewKey()
{
    CKey キー;
    キー.MakeNewKey();
    if (!AddKey(キー))
        throwruntime_error("GenerateNewKey(): AddKey が失敗しました\n");
    key.GetPubKey() を返します。
}

この方法では、次の手順に従って新しい公開鍵ペアを生成します。

  • まず、新しい CKey 型のオブジェクトを作成します (13 行目)。

  • addKey() メソッドを呼び出して、新しく作成されたキーを 1) グローバル マップmapKeys (行 5)、2) グローバルmap mapPubKeys (行 6)、およびウォレット データベース wallet.dat (行 8) に追加します。

mapKeys は、公開鍵と秘密鍵の間に 1 対 1 の対応を確立します。
mapPubKeys は、公開鍵のハッシュと公開鍵自体の間の対応を確立します。

  • 公開鍵を返します(16行目)。

公開鍵は、OpenSSL 標準形式の 1 つである非圧縮形式です。公開鍵を取得した後、Bitcoin クライアントは公開鍵をPubKeyToAddress()に渡し、 Hash160ToAddress()メソッドを呼び出してアドレスを生成します。最終的に返される Base58 でエンコードされた文字列値は、新しく生成された Bitcoin アドレスです。 Base58 は、i、l、0、o を除く 1 ~ 9 と英語の文字で構成されます。

CTransaction クラス

CTransaction の定義は main.h にあります。ビットコインでは、通貨の概念は実際には一連のトランザクション Tx の組み合わせです。この方法は実装が複雑ですが、ビットコインのセキュリティが向上します。ユーザーは取引ごとに新しいアドレスを作成することができ、そのアドレスは一度使用するとすぐに無効にすることができます。したがって、CTransaction は Bitcoin クライアントで最も重要なクラスの 1 つです。

クラス CTransaction
{
公共:
    int バージョン;
    ベクトル<CTxIn> vin;
    ベクトル<CTxOut> vout;
    int ロック時間;
    //.......
}

CTransaction には、入力トランザクション vin と出力トランザクション vout の 2 つのコンテナ タイプが含まれます。各 vin は複数の CTxIn オブジェクトで構成され、各 vout は CTxOut オブジェクトで構成されます。

各トランザクション Tx の入力トランザクション (CTxIn クラス) には、別のトランザクション Tx の出力トランザクションをソース トランザクションとして参照する COutPoint オブジェクト prevout が含まれています。ソース トランザクションにより、現在のトランザクション Tx は別のトランザクションから使用可能なビットコインを取得できるようになります。トランザクション Tx には任意の入力トランザクションを含めることができます。

各トランザクションは、256 ビットの uint256 ハッシュによって一意に識別されます。ソース トランザクション TxSource 内の特定の出力トランザクションを参照するには、TxSource のハッシュと、出力トランザクション内の出力トランザクションの位置 n という 2 つの情報が必要です。これら 2 種類の情報が COutPoint クラスを構成します。 COutPointオブジェクトは、ソーストランザクションの出力トランザクションTxSource.vout[n]を指します。出力トランザクションが、 Tx.vin[i].prevoutなど、別のトランザクション Tx の位置 i にある入力トランザクションによって参照される場合、それを TxSource の n 番目の出力トランザクションを使用する Tx の i 番目の入力トランザクションと呼びます。

uint256 および uint160 クラス

これら 2 つの型の定義は uint.h にあります。 uint256 クラスには 256 ビットのハッシュが含まれます。これは長さ 256/32=8 の unsigned int 配列で構成されます。同様のデータ構造は uint160 であり、その定義は同じファイル内にあります。 SHA-256 の長さは 256 ビットなので、uint160 の機能は RIPEMD-160 ハッシュを格納することであると読者が推測するのは難しくありません。 uint256 と uint160 はどちらも base_uint クラスから継承されます。

クラスbase_uint {
保護されています:
    列挙型 { 幅 = ビット / 32 };
    符号なし整数pn[幅];
公共:
    ブール演算子!() 定数
    {
        (int i = 0; i < WIDTH; i++) の場合
            (pn[i] != 0)の場合
                false を返します。
        true を返します。
    }
    //.......
    符号なし int GetSerializeSize(int nType = 0, int nVersion = VERSION) 定数
    {
        sizeof(pn) を返します。
    }
    テンプレート <typename Stream>
    void Serialize(Stream& s, int nType = 0, int nVersion = VERSION) const
    {
        s.write((char*)pn, sizeof(pn));
    }
    テンプレート <typename Stream>
    void アンシリアル化(Stream& s, int nType = 0, int nVersion = VERSION)
    {
        s.read((char*)pn, sizeof(pn));
    }
}

このクラスはいくつかの演算子をオーバーロードします。さらに、このクラスには、 GetSerializeSize()Serialize()Unserialize() 3 つのシリアル化メンバー関数があります。これら 3 つの方法がどのように機能するかについては、後で説明します。

送金()

このメソッドは main.cpp にあります。以下はこのメソッドのソースコードです。

 bool SendMoney(CScript scriptPubKey、int64 nValue、CWalletTx& wtxNew)
{
    CRITICAL_BLOCK(cs_main)
    {
        int64 n料金が必要;
        if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired))
        {
            文字列 strError;
            (nValue + nFeeRequired > GetBalance()) の場合
                strError = strprintf("エラー: これは、%s の取引手数料を必要とする大きすぎる取引です ", FormatMoney(nFeeRequired).c_str());
            それ以外
                strError = "エラー: トランザクションの作成に失敗しました ";
            wxMessageBox(strError, "送信中...");
            エラーを返します("SendMoney() : %s\n", strError.c_str());
        }
        if (!CommitTransactionSpent(wtxNew))
        {
            wxMessageBox("トランザクションの終了エラー", "送信中...");
            return error("SendMoney(): トランザクションの終了エラー");
        }
        printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str());
        // 放送
        (!wtxNew.AcceptTransaction()) の場合
        {
            // これは失敗してはいけません。取引はすでに署名され、記録されています。
            throwruntime_error("SendMoney() : wtxNew.AcceptTransaction() が失敗しました\n");
            wxMessageBox("エラー: トランザクションが無効です", "送信中...");
            return error("SendMoney() : エラー: トランザクションが無効です");
        }
        wtxNew.RelayWalletTransaction();
    }
    メインフレームの再ペイント();
    true を返します。
}

ユーザーがアドレスにビットコインを送信すると、Bitcoin クライアントは SendMoney() メソッドを呼び出します。このメソッドには 3 つのパラメータが含まれます。

  • scriptPubKey には、スクリプト コード OP_DUP OP_HASH160 <受取人アドレスの 160 ビット ハッシュ> OP_EQUALVERIFY OP_CHECKSIG が含まれています。

  • nValue は転送される金額を表します。この金額には取引手数料nTrasactionFeeは含まれません。

  • wtxNew は CWalletTx クラスのローカル変数です。変数は現在空で、いくつかの CMerkleTX クラス オブジェクトが含まれます。このクラスは CTransaction から派生し、いくつかのメソッドが追加されています。今のところ具体的な詳細は無視して、CTransaction クラスとして考えてみましょう。

この方法の流れは明らかです。

  • まず、新しいトランザクションを作成します (CreateTransaction(scriptPubKey, nValue, wtxNet, nFeeRequired)、6 行目)。

  • トランザクションをデータベースにコミットしてみます (CommitTransactionSpent(wtxNet)、行 16)。

  • トランザクションが正常に送信された場合 (wtxNew.AcceptTransaction()、行 23)、他のピア ノードにブロードキャストされます (wtxNew.RelayWalletTransaction()、行 30)。

これら 4 つのメソッドはすべて wtxNew に関連しています。この章では最初の 1 つを取り上げ、残りの 3 つについては後続の記事で取り上げます。

トランザクションの作成()

このメソッドは main.cpp にあります。以下はこのメソッドのソースコードです。

 bool CreateTransaction(CScript scriptPubKey、int64 nValue、CWalletTx& wtxNew、int64& nFeeRequiredRet)
{
    手数料が必要 = 0;
    CRITICAL_BLOCK(cs_main)
    {
        // mapWallet ロックの前に txdb を開く必要があります
        CTxDB txdb("r");
        CRITICAL_BLOCK(cs_mapWallet)
        {
            int64 nFee = nトランザクション手数料;
            ループ
            {
                wtxNew.vin.clear();
                wtxNew.vout.clear();
                (n値 < 0)の場合
                    false を返します。
                int64 nValueOut = nValue;
                n値 += n料金;
                // 使用するコインを選択する
                <CWalletTx*> にCoinsを設定します。
                if (!SelectCoins(nValue, setCoins))
                    false を返します。
                int64 nValueIn = 0;
                foreach(CWalletTx* pcoin、setCoins)
                    nValueIn += pcoin->GetCredit();
                // 受取人へのvout[0]を入力します
                wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey));
                // 変更があればvout[1]をselfに戻す
                (nValueIn > nValue) の場合
                {
                    // コインの1つと同じキーを使用する
                    ベクトル<unsigned char> vchPubKey;
                    CTransaction& txFirst = *(*setCoins.begin());
                    foreach(const CTxOut& txout, txFirst.vout)
                        txout.IsMine() の場合
                            (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) の場合
                                壊す;
                    (vchPubKey.empty()) の場合
                        false を返します。
                    // vout[1]を自分自身に埋め込む
                    CScript スクリプトPubKey;
                    scriptPubKey << vchPubKey << OP_CHECKSIG;
                    wtxNew.vout.push_back(CTxOut(nValueIn - nValue、scriptPubKey));
                }
                // ビンを埋める
                foreach(CWalletTx* pcoin、setCoins)
                    (int nOut = 0; nOut < pcoin->vout.size(); nOut++) の場合
                        (pcoin->vout[nOut].IsMine()) の場合
                            wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut));
                //サイン
                int nIn = 0;
                foreach(CWalletTx* pcoin、setCoins)
                    (int nOut = 0; nOut < pcoin->vout.size(); nOut++) の場合
                        (pcoin->vout[nOut].IsMine()) の場合
                            署名に署名します(*pcoin、wtxNew、nIn++);
                // 十分な手数料が含まれているか確認する
                (nFee < wtxNew.GetMinFee(true)) の場合
                {
                    nFee = nFeeRequiredRet = wtxNew.GetMinFee(true);
                    続く;
                }
                // 以前のトランザクションからコピーして vtxPrev を埋める vtxPrev
                wtxNew.AddSupportingTransactions(txdb);
                wtxNew.fTimeReceivedIsTxTime = true;
                壊す;
            }
        }
    }
    true を返します。
}

このメソッドを呼び出すときは、次の 4 つのパラメータが必要です。

  • scriptPubKey には、スクリプト コード OP_DUP OP_HASH160 <受取人アドレスの 160 ビット ハッシュ> OP_EQUALVERIFY OP_CHECKSIG が含まれています。

  • nValue は送金する金額であり、取引手数料 nTransactionFee は含まれません。

  • wtxNew は新しい Tx インスタンスです。

  • nFeeRequiredRet は、このメソッドの実行が完了した後に取得される、トランザクション手数料の支払いに使用される出力トランザクションです。

この方法のプロセスは次のとおりです。

  • 転送する金額を保持するためのローカル変数 nValueOut = nValue を定義します (17 行目)。取引手数料 nFee に nValue を加算して、送金手数料を含む新しい nValue を取得します。

  • 21 行目の SelectCoins(nValue, setCoins) を実行して、一連のコインを取得し、setCoins に格納します。 setCoins には、自分のアドレス、つまり自分が所有するコインに支払われるトランザクションが含まれます。これらのトランザクションは wtxNew のソース トランザクションになります。

  • 27 行目で wtxNew.vout.push_back(CTxOut (nValueOut,sciptPubKey)) を実行し、出力トランザクションを wtxNew に追加します。この出力は、<受信者アドレスの 160 ビット ハッシュ> (scriptPubKey に含まれる) 量のコインを支払います。

  • 変更が必要な場合 (nValueIn > nValue)、別の出力トランザクションを wtxNew に追加し、変更をユーザーに送り返します。このプロセスは次のステップで構成されます。

    • setCoin から最初のトランザクション txFirst を取得し、txFirst.vout 内のトランザクションが自分のものかどうかを確認します。はいの場合は、出力トランザクションから公開鍵を抽出し、ローカル変数vchPubKeyに格納します。

    • vchPubKey をスクリプト vchPubKey OP_CHECKSIG に配置し、このスクリプト コードを使用して、私に支払う wtxNew の出力トランザクションを追加します (行 45)。

    • setCoins には私に支払われるトランザクションが含まれるため、すべてのトランザクションには私に支払われるトランザクションが少なくとも 1 つ含まれている必要があります。最初のトランザクション txFirst から見つけることができます。

  • この時点で、wtxNew の出力トランザクション コンテナ vout の準備が整います。ここで、セットアップはトランザクション コンテナ VIN に入ります。各入力トランザクション リスト vin はソース トランザクションを参照し、wtxNew の各ソース トランザクションは setCoins で見つかることに注意してください。 setCoins内の各トランザクションpcoinについて、その出力トランザクションpcoin->vout[nOut]を1つずつ走査します。 nOutth 出力が所有者に支払われる場合 (つまり、wtxNew がこの出力トランザクションからコインを取得する場合)、wtxNew に新しい入力トランザクションを追加します (wtxNew.vin(wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut))、行 51)。この入力トランザクションは、pcoin の nOutth 出力トランザクションを指しているため、wtxNew.vin は pcoin の nOutth 出力に接続されます。

  • setCoins内の各トランザクションpcoinについて、そのすべての出力トランザクションpcoin->vout[nOut]を1つずつ走査します。トランザクションが自分のものである場合は、SignSignature(*pcoin,wtxNew, nIn++) を呼び出して、nIn 番目の入力トランザクションに署名を追加します。 nIn は wtxNew の入力トランザクション位置であることに注意してください。

  • トランザクション手数料 nFee が wtxNet.GetMinFee(true) より小さい場合は、nFee を後者に設定し、wtxNew 内のすべてのデータをクリアして、プロセス全体を再開します。 11 行目の最初の反復では、nFee はグローバル変数 nTransactionFee = 0 のローカル コピーです。

  • wtxNew を補充するのになぜそれほどの労力がかかるのか分からない場合は、ソース コードの GetMinFee() を見ると答えがわかります。トランザクションの最小手数料は、トランザクションのデータ サイズに関連しています。 wtxNew のサイズは、完全に構築されるまでわかりません。 wtxNew.GetMinFee(true) によって計算された最小トランザクション手数料が、wtxNew の作成時に想定されたトランザクション手数料 nFee よりも大きい場合は、wtxNew を再構築する以外に方法はありません。

  • これは、鶏が先か卵が先かという状況を生み出します。つまり、新しいトランザクションを作成する場合、トランザクション手数料がいくらかを知っておく必要があります。取引手数料は、取引全体が作成された後にのみ判明します。このループを解消するには、ローカル変数 nFee を使用して推定トランザクション手数料を配置し、その上に新しいトランザクションを構築します。ビルドが完了したら、実際の取引手数料を取得し、推定取引手数料と比較します。見積取引手数料が実際の取引手数料より少ない場合、実際の取引手数料に置き換えられ、取引全体が再構築されます。

GetMinFee() のソースコードは次のとおりです。

 int64 GetMinFee(bool fDiscount=false) 定数
    {
        符号なし整数 nBytes = ::GetSerializeSize(*this, SER_NETWORK);
        (fDiscount && nBytes < 10000)の場合
            0を返します。
        (1 + (int64)nBytes / 1000) * CENTを返します。
    }
  • 計算された取引手数料が以前に見積もられた取引手数料よりも高い場合、11 行目から始まるループが終了し、関数全体が返されます (67 行目)。その前に、次の 2 つの手順が必要です。

    • wtxNew.AddSupportingTransactions(txdb) を実行します。この部分については後ほど詳しく紹介します。

    • wtxNet.fTimeReceivedIsTxTime = true を設定します (行 66)。

ここで、SignSignature() を使用して新しく生成されたトランザクション wtxNew に署名する方法を見てみましょう。

署名署名()

このメソッドは script.cpp にあります。以下はこのメソッドのソースコードです。

 bool 署名署名(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript スクリプト事前要件)
{
    アサート(nIn < txTo.vin.size());
    CTxIn& txin = txTo.vin[nIn];
    txin.prevout.n < txFrom.vout.size() をアサートします。
    const CTxOut& txout = txFrom.vout[txin.prevout.n];
    // 署名は自身に署名できないため、ハッシュから署名を除外します。
    // checksig オペレーションはハッシュから署名も削除します。
    uint256 ハッシュ = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);
    if (!Solver(txout.scriptPubKey, ハッシュ, nHashType, txin.scriptSig))
        false を返します。
    txin.scriptSig = scriptPrereq + txin.scriptSig;
    // テストソリューション
    (scriptPrereq.empty()) の場合
        if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn))
            false を返します。
    true を返します。
}

最初に注意すべき点は、この関数には 5 つのパラメータがあるのに対し、 CreateTransaction()には 3 つしかないことです。これは、script.h ファイルでは、最後の 2 つのパラメータがデフォルトで指定されているためです。

CreateTransaction()に渡される 3 つのパラメータは次のとおりです。

  • txFrom は *pcoin オブジェクトです。これは、CreateTransaction() の setCoins 内のコインの 1 つです。それはソース取引でもあります。出力トランザクションの一部には、新しいトランザクションで使用されるコインが含まれています。

  • txTo は CreateTransaction() 内の wtxNew オブジェクトです。ソース トランザクション txFrom を使用するのは新しいトランザクションです。新しいトランザクションは、有効になる前に署名される必要があります。

  • nIn は、txTo 内の入力トランザクション リストを指すインデックス位置です。入力トランザクション リストには、txFrom の出力トランザクション リストへの参照が含まれています。より正確には、txin=txTo.vin[nIn] (行 4) は txTo の入力トランザクションです。 txout=txFrom.vout[txin.prev.out.n] (6行目)は、txinが指すtxFromの出力トランザクションです。

SignSignature() の機能は次のとおりです:

  • SignatureHash() メソッドを呼び出して、txTo のハッシュ値を生成します。

  • Solver() 関数を呼び出して、生成されたハッシュに署名します。

  • EvalScript() を呼び出して小さなスクリプトを実行し、署名が有効かどうかを確認します。

これら 3 つの機能を一緒に見てみましょう。

署名ハッシュ()

このメソッドはscript.cppにあります。以下はSignatureHash()のソースコードです。

 uint256 SignatureHash(CScript スクリプトコード、const CTransaction& txTo、unsigned int nIn、int nHashType)
{
    (nIn >= txTo.vin.size()) の場合
    {
        printf("エラー: SignatureHash(): nIn=%d が範囲外です\n", nIn);
        1 を返します。
    }
    CトランザクションtxTmp(txTo);
    // 2つのスクリプトを連結すると2つのコードセパレータが生成される場合、
    // または最後にもう 1 つ追加すると、すべての非互換性が回避されます。
    scriptCode.FindAndDelete(CScript(OP_CODESEPARATOR));
    // 他の入力の署名を空白にする
    (int i = 0; i < txTmp.vin.size(); i++) の場合
        txTmp.vin[i].scriptSig = CScript();
    txTmp.vin[nIn].scriptSig = スクリプトコード;
    // 出力の一部を空白にする
    ((nHashType & 0x1f) == SIGHASH_NONEの場合)
    {
        // ワイルドカード受取人
        txTmp.vout.clear();
        // 他の人が自由に更新できるようにする
        (int i = 0; i < txTmp.vin.size(); i++) の場合
            もし (i != nIn)
                txTmp.vin[i].nシーケンス = 0;
    }
    そうでない場合 ((nHashType & 0x1f) == SIGHASH_SINGLE)
    {
        // txin と同じインデックスの txout 受取人のみをロックインします
        符号なし整数 nOut = nIn;
        (nOut >= txTmp.vout.size()) の場合
        {
            printf("エラー: SignatureHash() : nOut=%d が範囲外です\n", nOut);
            1 を返します。
        }
        txTmp.vout.resize(nOut+1);
        (int i = 0; i < nOut; i++) の場合
            txTmp.vout[i].SetNull();
        // 他の人が自由に更新できるようにする
        (int i = 0; i < txTmp.vin.size(); i++) の場合
            もし (i != nIn)
                txTmp.vin[i].nシーケンス = 0;
    }
    // 他の入力を完全に空白にします。オープントランザクションには推奨されません。
    (nHashType & SIGHASH_ANYONECANPAY)の場合
    {
        txTmp.vin[0] = txTmp.vin[nIn];
        txTmp.vin.resize(1);
    }
    // シリアル化してハッシュする
    CDataStream ss(SER_GETHASH);
    ss.reserve(10000);
    ss << txTmp << nHashType;
    ハッシュ(ss.begin(), ss.end())を返します。
}

この関数に必要なパラメータは次のとおりです。

  • txTo は署名するトランザクションです。これは CreateTransaction() の wtxNew オブジェクトでもあります。入力トランザクション リストtxTo.vin[nIn]の nIn 番目の項目は、関数が動作するターゲットです。

  • scriptCode は scriptPrereq + txout.scriptPubKey です。ここで、txout は SignSignature() で定義されたソース トランザクション txFrom() の出力トランザクションです。現時点では scriptPrereq は空なので、scriptCode は実際には、ソース トランザクション txFrom の出力トランザクション リスト内の入力トランザクションとして txTo によって参照されるトランザクションのスクリプト コードです。 txout.scriptPubKey には 2 種類のスクリプトが含まれる場合があります。

    • スクリプト A: OP_DUP OP_HASH160 <アドレスの 160 ビット ハッシュ> OP_EQUALVERIFY OP_CECKSIG。このスクリプトは、ソース トランザクション txFrom でコインを送信します。ここで、<アドレスの 160 ビット ハッシュ> は Bitcoin アドレスです。

    • スクリプト B: <公開鍵> OP_CHECKSIG。このスクリプトは、残りのコインをソース トランザクション txFrom のイニシエーターに返します。作成する新しいトランザクション txTo/wtxNew は txFrom からのコインを使用するため、txFrom の作成者でもある必要があります。つまり、txFrom を作成すると、実際には他の人が以前に送信したコインを使用していることになります。したがって、<あなたの公開鍵> は、txFrom の作成者の公開鍵であると同時に、あなた自身の公開鍵でもあります。

ここで少し立ち止まって、スクリプト A とスクリプト B について考えてみましょう。これらのスクリプトはどこから来たのかと疑問に思うかもしれません。サトシ・ナカモトがビットコインを作成したとき、スクリプト言語システムを追加したため、ビットコインのすべてのトランザクションはスクリプト コードによって完了します。このスクリプト システムは、実際には後のスマート コントラクトのプロトタイプです。スクリプト A はメソッド CSendDialog::OnButtonSend() の 29 行目から取得され、スクリプト B はメソッド CreateTransaction() の 44 行目から取得されます。

  • ユーザーがトランザクションを開始すると、Bitcoin クライアントは CSendDialog::OnButtonSend() メソッドを呼び出し、txFrom の出力トランザクションにスクリプト A を追加します。この出力トランザクションの受信者はあなたなので、スクリプト内の <受信者のアドレスの 160 ビット ハッシュ> は <あなたのアドレスの 160 ビット ハッシュ> になります。

  • txFrom がユーザーによって作成された場合、スクリプト B は CreateTransaction() の txFrom の出力トランザクションに追加されます。ここで、44 行目の CreateTransaction() の公開鍵 vchPubKey は、独自の公開鍵です。

入力トランザクションを理解した後、SignatureHash() がどのように機能するかを理解しましょう。

SignatureHash() は、まず txTO を txTmp にコピーし、次に txTmp.vin 内の各入力トランザクションの scriptSig をクリアします。ただし、txTmp.vin[nIn] の場合は除きます。txTmp.vin[nIn] では、入力トランザクションの scriptSig が scriptCode に設定されます (14 行目と 15 行目)。

次に、関数は nHashType の値をチェックします。この関数の呼び出し元は、関数 nHashType = SIGHASH_ALL に列挙値を渡します。

列挙型
{
    SIGHASH_ALL = 1、
    SIGHASH_NONE = 2、
    SIGHASH_SINGLE = 3、
    SIGHASH_ANYONECANPAY = 0x80、
};

nHashType = SIGHASH_ALL なので、すべての if-else 条件は満たされず、関数は最後の 4 行のコードを直接実行します。

コードの最後の 4 行では、txTmp と nHashType が CDataStream 型のシリアル化されたオブジェクトになります。この型は、データを保持する文字コンテナ型で構成されます。返されるハッシュ値は、シリアル化されたデータを計算した Hash() メソッドの結果です。

トランザクションには複数の入力トランザクションを含めることができます。 SignatureHash() はエントリの 1 つをターゲットとして受け取ります。以下の手順に従ってハッシュを生成します。

  • 対象トランザクションを除くすべての入力トランザクションをクリアします。

  • ターゲット トランザクションによって入力トランザクションとして参照されるソース トランザクション内の出力トランザクションのスクリプトを、ターゲット トランザクションの入力トランザクション リストにコピーします。

  • 変更されたトランザクションのハッシュ値を生成します。

ハッシュ()

このメソッドは util.h にあります。以下はハッシュ値を生成する Hash() メソッドのソース コードです。

テンプレート<typename T1>
インライン uint256 ハッシュ(const T1 pbegin、const T1 pend)
{
    uint256 ハッシュ1;
    SHA256((unsigned char*)&pbegin[0], (pend - pbegin) * sizeof(pbegin[0]), (unsigned char*)&hash1);
    uint256 ハッシュ2;
    SHA256((unsigned char*)&hash1, sizeof(hash1), (unsigned char*)&hash2);
    ハッシュ2を返します。
}

この関数は、対象データに対して SHA256() メソッドを 2 回実行し、結果を返します。 SHA256() の宣言は openssl/sha.h にあります。

ソルバー()

このメソッドは script.cpp にあります。 Solver() は、SignatureHash() の直後の SignSignature() 内で実行されます。これは、SignatureHash() によって返されるハッシュ値の署名を実際に生成する関数です。

 bool ソルバー(const CScript& scriptPubKey, uint256 ハッシュ, int nHashType, CScript& scriptSigRet)
{
    scriptSigRet.clear();
    ベクトル<ペア<オペコードタイプ、値タイプ> > vSolution;
    if (!Solver(scriptPubKey, vSolution))
        false を返します。
    // ソリューションをコンパイルする
    CRITICAL_BLOCK(cs_mapKeys)
    {
        foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution)
        {
            (item.first == OP_PUBKEY)の場合
            {
                //サイン
                const valtype& vchPubKey = item.second;
                (!mapKeys.count(vchPubKey)) の場合
                    false を返します。
                ハッシュが 0 の場合
                {
                    ベクトル<unsigned char> vchSig;
                    if (!CKey::Sign(mapKeys[vchPubKey], ハッシュ, vchSig))
                        false を返します。
                    vchSig.push_back((unsigned char)nHashType);
                    スクリプトSigRet << vchSig;
                }
            }
            そうでない場合 (item.first == OP_PUBKEYHASH)
            {
                // 署名して公開鍵を渡す
                map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second));
                mi == mapPubKeys.end() の場合
                    false を返します。
                const vector<unsigned char>& vchPubKey = (*mi).second;
                (!mapKeys.count(vchPubKey)) の場合
                    false を返します。
                ハッシュが 0 の場合
                {
                    ベクトル<unsigned char> vchSig;
                    if (!CKey::Sign(mapKeys[vchPubKey], ハッシュ, vchSig))
                        false を返します。
                    vchSig.push_back((unsigned char)nHashType);
                    scriptSigRet << vchSig << vchPubKey;
                }
            }
        }
    }
    true を返します。
}

このメソッドに必要な 4 つのパラメータは次のとおりです。

  • 10 行目の呼び出し関数 SignSignature() は、ソース トランザクション txFrom の出力スクリプトである txOut.scriptPubKey を、最初のパラメータ scriptPubKey への入力値として渡します。スクリプト A またはスクリプト B が含まれる可能性があることに留意してください。

  • 2 番目のパラメータ hash は、SignatureHash() によって生成されたハッシュ値です。

  • 3 番目のパラメータ nHashType の値は SIGHASH_ALL です。

  • 4 番目のパラメーターは関数の戻り値であり、呼び出し関数 SignSIGNATURE() の 12 行目にある txin.scriptSig です。 txin は新しく生成されたトランザクション wtxNew (関数 SignSignature() の呼び出しでは txTo として参照されます) であり、nIn の入力トランザクションであることを覚えておいてください。したがって、wtxNew の nInth 入力トランザクションの scriptSig には、この関数によって返された署名が格納されます。

この関数は、まず 2 つのパラメータを持つ別の Solver() を呼び出します。調べてみましょう。

2 つのパラメータを持つ Solver()

このメソッドは script.cpp にあります。以下は 2 つのパラメータを持つ Solver() のソース コードです。

 bool ソルバー(const CScript& scriptPubKey, ベクトル<pair<opcodetype, valtype> >& vSolutionRet)
{
    // テンプレート
    静的ベクター<CScript> vTemplates;
    (vTemplates.empty()) の場合
    {
        // 標準の送信、送信者が公開鍵を提供し、受信者が署名を追加する
        vTemplates.push_back(CScript() << OP_PUBKEY << OP_CHECKSIG);
        // 短いアカウント番号の送信、送信者は公開鍵のハッシュを提供し、受信者は署名と公開鍵を提供する
        vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG);
    }
    // テンプレートをスキャンする
    定数 CScript&script1 = scriptPubKey;
    foreach(const CScript& script2, vTemplates)
    {
        vSolutionRet.clear();
        オペコードタイプ オペコード1、オペコード2;
        ベクトル<unsigned char> vch1, vch2;
        // 比較する
        CScript::const_iterator pc1 = script1.begin();
        CScript::const_iterator pc2 = script2.begin();
        ループ
        {
            ブール f1 = script1.GetOp(pc1, opcode1, vch1);
            ブール f2 = script2.GetOp(pc2, opcode2, vch2);
            (!f1 && !f2) の場合
            {
                // 成功
                逆順(vSolutionRet.begin(), vSolutionRet.end());
                true を返します。
            }
            そうでない場合 (f1 != f2)
            {
                壊す;
            }
            そうでない場合 (opcode2 == OP_PUBKEY)
            {
                (vch1.s​​ize() <= sizeof(uint256)) の場合
                    壊す;
                vSolutionRet.push_back(make_pair(opcode2, vch1));
            }
            そうでない場合 (opcode2 == OP_PUBKEYHASH)
            {
                (vch1.s​​ize() != sizeof(uint160)) の場合
                    壊す;
                vSolutionRet.push_back(make_pair(opcode2, vch1));
            }
            そうでない場合 (opcode1 != opcode2)
            {
                壊す;
            }
        }
    }
    vSolutionRet.clear();
    false を返します。
}

最初のパラメータ scriptPubKey には、スクリプト A またはスクリプト B を含めることができます。これも、SignSignature() のソース トランザクション txFrom の出力スクリプトです。

2 番目のパラメータは、出力トランザクションを保存するために使用されます。これはコンテナのペアであり、各ペアはスクリプト演算子 (opcodetype 型) とスクリプトオペランド (valtype 型) で構成されます。

この関数の 8 行目から 10 行目では、まず 2 つのテンプレートを定義します。

  • テンプレート A: OP_DUP OP_HASH160 OP_PUBKEYHASH OP_EQUALVERIFY OP_CHECKSIG。

  • テンプレート B: OP_PUBKEY OP_CHECKSIG。

明らかに、テンプレート A とテンプレート B はスクリプト A とスクリプト B に対応しています。比較のために、スクリプト A と B の内容を以下に示します。

  • スクリプト A: OP_DUP OP_HASH160 <アドレスの 160 ビット ハッシュ> OP_EQUALVERIFY OP_CHECKSIG。

  • スクリプト B: <公開鍵> OP_CHECKSIG。

この関数は、scriptPubKey を 2 つのテンプレートと比較します。

  • 入力スクリプトがスクリプト A の場合、テンプレート A の OP_PUBKEYHASH とスクリプト A の <あなたのアドレスの 160 ビット ハッシュ> をペアにして、そのペアを vSolutionRet に格納します。

  • 入力スクリプトがスクリプト B の場合、テンプレート B から演算子 OP_PUBKEY を抽出し、スクリプト B からオペランド <公開キー> を抽出し、それらをペアにして vSolutionRet に格納します。

  • 入力スクリプトがどちらのテンプレートとも一致しない場合は、false を返します。

ソルバーに戻る()

4 つのパラメータを使用して Solver() に戻り、この関数の分析を続けます。これで、この機能がどのように動作するかがわかりました。 vSolutionRet から受信したクエリがスクリプト A からのものかスクリプト B からのものかに応じて、実行する 2 つのブランチのいずれかを選択します。スクリプト A からのものであれば、item.first == OP_PUBKEYHASH; です。スクリプト B からのものである場合、item.first == OP_PUBKEY です。

  • item.first == OP_PUBKEY (スクリプト B)。この場合、item.second には <公開鍵> が含まれます。グローバル変数 mapKeys は、すべての公開鍵を対応する秘密鍵にマッピングします。公開鍵が mapKeys に存在しない場合は、エラーが報告されます (行 16)。それ以外の場合、新しく生成されたトランザクション wtxNew のハッシュ値は、mapKeys から抽出された秘密鍵で署名され、ハッシュ値は 2 番目のパラメーター (CKey::Sign(mapKeys[vchPubKey], hash, vchSig)、行 23) として渡され、結果が vchSig に格納され、scriptSigRet (scriptSigRet << vchSig、行 24) にシリアル化されて返されます。

  • item.first == OP_PUBKEYHASH (スクリプト A)。この場合、item.second には <アドレスの 160 ビット ハッシュ> が含まれます。この Bitcoin アドレスは、23 行目にあるグローバル マップ mapPubKeys から対応する公開鍵を見つけるために使用されます。グローバル マップ mapPubKeys は、アドレスとそれを生成した公開鍵の間に 1 対 1 の対応を確立します (関数 AddKey() を参照)。次に、公開キーを使用して mapKeys から対応する秘密キーを見つけ、秘密キーを使用して 2 番目のパラメータ ハッシュに署名します。署名と公開鍵は scriptSigRet に一緒にシリアル化され、返されます (scriptSig << vchSig << vchPubkey、行 24)。

評価スクリプト()

このメソッドは script.cpp にあります。それでは、SignSignature() に戻りましょう。関数の 12 行目以降、wtxNew の nInth 入力トランザクションの scriptSig 部分である txin.scriptsig が署名を挿入します。署名は次のいずれかになります。

  • vchSig vchPubKey (スクリプトAの署名A)

  • vchSig (スクリプト B の署名 B)

以下のテキストでは、vchSig は <your signature_vchSig> として参照され、vchPubKey は <your public key_vchPubKey> として参照され、それぞれがあなたの署名と公開鍵であることを強調します。

次に、SignSignature() によって最後に呼び出される関数である EvalScript() の調査を開始します。この関数は 15 行目にあります。EvalScript() は 3 つのパラメータを取ります。

  • 最初のパラメータは、txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey です。それは次のようになります:

    • 検証シナリオ A: <署名_vchSig> <公開鍵_vchPubKey> OP_CODESEPARATOR OP_DUP OP_HASH160 <アドレスの 160 ビット ハッシュ> OP_EQUALVERIFY OP_CHECKSIG、つまり、署名 A + OP_CODESEPARATOR + スクリプト A。

    • 検証シナリオ B: <署名_vchSig> OP_CODESEPARATOR <公開鍵_vchPubKey> OP_CHECKSIG、つまり、署名 B + OP_CODESEPARATOR + スクリプト B。

  • 2 番目のパラメーターは、新しく作成されたトランザクション txTo で、CreateTransaction() では wtxNew です。

  • 3 番目のパラメータは nIn で、これは txTo 入力トランザクション リスト内で検証されるトランザクションの位置です。

検証プロセスについては後ほど詳しく説明します。簡単に言えば、EvalScript() は、新しく作成されたトランザクション wtxNew の nInth 入力トランザクションに有効な署名が含まれているかどうかを確認します。この時点で、新しいビットコイントランザクションが作成されます。

<<:  ビットコインがバブルかどうかを見分ける4つの方法

>>:  アリババは偽造品対策に乗り出し、食品詐欺対策にブロックチェーンを使い始める

推薦する

JPモルガン・チェース:ビットコインが最近55,000ドルに戻った3つの理由

アメリカの大手銀行JPモルガン・チェースはレポートの中で、ビットコインが最近55,793ドルという数...

イーサリアム開発者はブロック報酬遅延「難易度爆弾」を削減することを決定

Coindeskによると、本日、14人のイーサリアム開発者がビデオ会議で、提案されている「ディフィカ...

Ethereum Foundation との会話の全文: 1559 は延期されるでしょうか? POWはいつ終わりますか?サイドチェーンの競争を心配していますか?

ウー・サイード著者 |コリン・ウーこの号の編集者 |コリン・ウーウー氏は、暗号化されていない会話が私...

BCH コンピューティングパワー戦争についてどう思いますか?リアルタイムバトルテーブル

昨日の朝、BCHABC BCHSV ハッシュレート戦争が正式に始まりました。両者は激しい戦闘を開始し...

ビットコインエコシステムの開発者をサポートしているのは誰ですか?

ビットコインの開発について語るとき、まず頭に浮かぶのは、2018年にビットコインのホワイトペーパー「...

専門家は、患者が電子医療記録を直接管理できる医療ブロックチェーンアプリケーションを構想している。

クレイジーな解説: 新しいテクノロジーが登場すると、ブロックチェーン テクノロジーを現在のネットワー...

F2Poolが考えを変え、ビットコイン拡張プロトコルSegwit2xに変更が加えられる可能性

物議を醸しているスケーリングプロトコルSegwit2xをサポートするために以前にサインアップした中国...

ランキングが高ければ高いほどいいのでしょうか? Filecoinのペナルティメカニズムについて学びましょう

Filecoin に触れたことがあるマイナーは、Filecoin にペナルティ メカニズムがあるこ...

単年度の投資規模が過去すべての年の合計を上回ります。 Web3.0: 単なる誇大宣伝か、それともインターネットの未来か?

20 年前、プログラマーが HTML (ハイパーテキスト マークアップ言語) を使用してブログを書...

中央銀行は独自のビットコインを発行したい

ビットコインに関して、大手銀行は「本当に魅力的だが、同時に不安も感じる」と述べている。ここ数カ月、大...

日本の家電量販店大手、ビットコイン決済サービスを試験導入へ

【共同通信4月4日】家電量販店大手のビックカメラは5日、都内の旗艦店2店舗を試験的に活用し、仮想通貨...

PwCはブロックチェーンのPoCを成功させ、マルチチェーンプラットフォームを使用してブロックチェーンのリアルタイム監査プロセスを構築しました。

プライスウォーターハウスクーパース(PwC)は、新しいブロックチェーン概念実証(PoC)の詳細を発表...

過去からの教訓: イーサリアムよりも古いパブリックチェーンの現状はどうなっているのでしょうか?

最初の 1CO デジタル通貨は誰ですか?イーサリアムは最も初期のパブリックチェーンではないのですか?...

30日間でBNBが400%急騰した理由は何ですか?他にはどんな懸念がありますか?

ウー・サイード著者 |コリン・ウーこの号の編集者 |コリン・ウー「固体はすべて溶けて空気中に消え去り...

VCCoin、ブロックチェーンベースのVC業界

ブロックチェーンはますます多くの業界で応用されています。分散化、オープン性、自律性、情報の不変性、匿...