序文ブロックチェーンの基盤となるコアテクノロジーを理解せずにブロックチェーン開発に取り組むだけでは不十分です。多くの人は、ビットコインのホワイトペーパーを読んでも、ビットコインがどのように実装されているかをまだ知りません。これは、ビットコインのソースコードが巧妙に設計されており、ホワイトペーパーには記載されていない設計が多数あるためです。さらに、ビットコイン自体に関するドキュメントも少なく、初心者にとっては理解するのが難しくなっています。ブロックチェーンを紹介する書籍や記事は数多くありますが、ソースコードから分析したものはほとんどありません。ブロックチェーンを半年勉強した後、ビットコインのソースコードに関するチュートリアルを書き始めました。このチュートリアルは理解しやすく、最も古典的なブロックチェーンである 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() を返します。 } この方法では、次の手順に従って新しい公開鍵ペアを生成します。
mapKeys は、公開鍵と秘密鍵の間に 1 対 1 の対応を確立します。
公開鍵は、OpenSSL 標準形式の 1 つである非圧縮形式です。公開鍵を取得した後、Bitcoin クライアントは公開鍵を 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オブジェクトは、ソーストランザクションの出力トランザクション 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)); } } このクラスはいくつかの演算子をオーバーロードします。さらに、このクラスには、 送金()このメソッドは 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 つのパラメータが含まれます。
この方法の流れは明らかです。
これら 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 つのパラメータが必要です。
この方法のプロセスは次のとおりです。
GetMinFee() のソースコードは次のとおりです。 int64 GetMinFee(bool fDiscount=false) 定数 { 符号なし整数 nBytes = ::GetSerializeSize(*this, SER_NETWORK); (fDiscount && nBytes < 10000)の場合 0を返します。 (1 + (int64)nBytes / 1000) * CENTを返します。 }
ここで、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 つのパラメータがあるのに対し、
SignSignature() の機能は次のとおりです:
これら 3 つの機能を一緒に見てみましょう。 署名ハッシュ()このメソッドは 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())を返します。 } この関数に必要なパラメータは次のとおりです。
ここで少し立ち止まって、スクリプト A とスクリプト B について考えてみましょう。これらのスクリプトはどこから来たのかと疑問に思うかもしれません。サトシ・ナカモトがビットコインを作成したとき、スクリプト言語システムを追加したため、ビットコインのすべてのトランザクションはスクリプト コードによって完了します。このスクリプト システムは、実際には後のスマート コントラクトのプロトタイプです。スクリプト A はメソッド CSendDialog::OnButtonSend() の 29 行目から取得され、スクリプト B はメソッド CreateTransaction() の 44 行目から取得されます。
入力トランザクションを理解した後、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 つのパラメータは次のとおりです。
この関数は、まず 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.size() <= sizeof(uint256)) の場合 壊す; vSolutionRet.push_back(make_pair(opcode2, vch1)); } そうでない場合 (opcode2 == OP_PUBKEYHASH) { (vch1.size() != 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 とテンプレート B はスクリプト A とスクリプト B に対応しています。比較のために、スクリプト A と B の内容を以下に示します。
この関数は、scriptPubKey を 2 つのテンプレートと比較します。
ソルバーに戻る()4 つのパラメータを使用して Solver() に戻り、この関数の分析を続けます。これで、この機能がどのように動作するかがわかりました。 vSolutionRet から受信したクエリがスクリプト A からのものかスクリプト B からのものかに応じて、実行する 2 つのブランチのいずれかを選択します。スクリプト A からのものであれば、item.first == OP_PUBKEYHASH; です。スクリプト B からのものである場合、item.first == OP_PUBKEY です。
評価スクリプト()このメソッドは script.cpp にあります。それでは、SignSignature() に戻りましょう。関数の 12 行目以降、wtxNew の nInth 入力トランザクションの scriptSig 部分である txin.scriptsig が署名を挿入します。署名は次のいずれかになります。
以下のテキストでは、vchSig は <your signature_vchSig> として参照され、vchPubKey は <your public key_vchPubKey> として参照され、それぞれがあなたの署名と公開鍵であることを強調します。 次に、SignSignature() によって最後に呼び出される関数である EvalScript() の調査を開始します。この関数は 15 行目にあります。EvalScript() は 3 つのパラメータを取ります。
検証プロセスについては後ほど詳しく説明します。簡単に言えば、EvalScript() は、新しく作成されたトランザクション wtxNew の nInth 入力トランザクションに有効な署名が含まれているかどうかを確認します。この時点で、新しいビットコイントランザクションが作成されます。 |
>>: アリババは偽造品対策に乗り出し、食品詐欺対策にブロックチェーンを使い始める
アメリカの大手銀行JPモルガン・チェースはレポートの中で、ビットコインが最近55,793ドルという数...
Coindeskによると、本日、14人のイーサリアム開発者がビデオ会議で、提案されている「ディフィカ...
ウー・サイード著者 |コリン・ウーこの号の編集者 |コリン・ウーウー氏は、暗号化されていない会話が私...
昨日の朝、BCHABC BCHSV ハッシュレート戦争が正式に始まりました。両者は激しい戦闘を開始し...
ビットコインの開発について語るとき、まず頭に浮かぶのは、2018年にビットコインのホワイトペーパー「...
クレイジーな解説: 新しいテクノロジーが登場すると、ブロックチェーン テクノロジーを現在のネットワー...
物議を醸しているスケーリングプロトコルSegwit2xをサポートするために以前にサインアップした中国...
Filecoin に触れたことがあるマイナーは、Filecoin にペナルティ メカニズムがあるこ...
20 年前、プログラマーが HTML (ハイパーテキスト マークアップ言語) を使用してブログを書...
ビットコインに関して、大手銀行は「本当に魅力的だが、同時に不安も感じる」と述べている。ここ数カ月、大...
【共同通信4月4日】家電量販店大手のビックカメラは5日、都内の旗艦店2店舗を試験的に活用し、仮想通貨...
プライスウォーターハウスクーパース(PwC)は、新しいブロックチェーン概念実証(PoC)の詳細を発表...
最初の 1CO デジタル通貨は誰ですか?イーサリアムは最も初期のパブリックチェーンではないのですか?...
ウー・サイード著者 |コリン・ウーこの号の編集者 |コリン・ウー「固体はすべて溶けて空気中に消え去り...
ブロックチェーンはますます多くの業界で応用されています。分散化、オープン性、自律性、情報の不変性、匿...