【區塊鏈】比特幣原始碼學習
比特幣原始碼學習 - 1 - 交易
一、交易概念
1、 交易形式
比特幣交易中的基礎構建單元是交易輸出。在比特幣的世界裡既沒有賬戶,也沒有餘額,只有分散到區塊鏈裡的UTXO[未花費的交易輸出]。
例如,你有20比特幣的UTXO並且想支付1比特幣,那麼你的交易必須消耗掉整個20比特幣的UTXO並且產生兩個輸出:一個是支付了1比特幣給接收人,另一個是支付19比特幣的找零到你的錢包。
程式碼目錄:src/coins.h
// 一個基本的UTXO,未花費交易輸出
class Coin
{
public:
CTxOut out;
// 該UTXO是否是coinbase交易
unsigned int fCoinBase : 1;
// 包含該UTXO的交易所在區塊在區塊鏈上的高度
uint32_t nHeight : 31;
...
};
2、交易結構
程式碼目錄:src/primitives/transaction.h
A. 交易輸入
被消耗的UTXO
class CTxIn
{
public:
COutPoint prevout; // 前一個交易對應的輸出,該輸入引用的UTXO
CScript scriptSig; // 解鎖指令碼
uint32_t nSequence; // 指定交易什麼時候可以被寫到區塊鏈中
CScriptWitness scriptWitness; // ! 隔離見證
// 規則1:如果nSequence被賦值為這個值,交易立刻執行,nLockTime無效,無需考慮鎖定時間和要到達那個區塊號再執行
static const uint32_t SEQUENCE_FINAL = 0xffffffff;
// 規則2:如果設定了這個變數,那麼規則1就失效了[同交易立刻執行,nLockTime無效,無需考慮鎖定時間和要到達那個區塊號再執行]
static const uint32_t SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31);
// 規則3:如果規則1有效並且設定了此變數,那麼相對鎖定時間就為512秒,否則鎖定時間就為1個區塊
static const uint32_t SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22);
// 規則4:如果規則1有效,那麼這個變數就用來從nSequence計算對應的鎖定時間
static const uint32_t SEQUENCE_LOCKTIME_MASK = 0x0000ffff;
// 區塊生成時間:512 = 2^9
static const int SEQUENCE_LOCKTIME_GRANULARITY = 9;
...
};
其中COutPoint
: 用於定位該交易輸入的來源(UTXO),起到point的作用
class COutPoint
{
public:
uint256 hash; // UTXO所在的交易的雜湊值
uint32_t n; // UTXO的索引號
...
};
B. 交易輸出
由交易建立的UTXO,即未花費交易輸出
class CTxOut
{
public:
CAmount nValue; // 比特幣數量
CScript scriptPubKey; // 鎖定指令碼,決定了誰可以花費這筆UTXO
...
};
C. 交易
下面就是在網路中廣播然後被打包進區塊的最基本的交易的結構,一個交易可能包含多個交易輸入和輸出。
class CTransaction
{
public:
// 預設的交易版本資訊。0.13版本以前的程式碼是1,從0.14版本開始版本是2
static const int32_t CURRENT_VERSION=2;
// 標準交易的最高版本,為的是相容原來的版本。這個引數是從0.12的版本出現的。
static const int32_t MAX_STANDARD_VERSION=2;
const std::vector<CTxIn> vin; // 交易輸入
const std::vector<CTxOut> vout; // 交易輸出
const int32_t nVersion; // 版本
const uint32_t nLockTime; // 交易時間鎖,用來控制交易的輸出只有在一段時間後才能被花費
private:
/** Memory only. */
const uint256 hash;
const uint256 m_witness_hash;
uint256 ComputeHash() const;
uint256 ComputeWitnessHash() const;
...
};
二、交易指令碼
1、基本概念
前面提到交易的基本結構,我們注意到比特幣流從一個交易流動到了另一個交易,像這樣一直傳遞下去。CTxIn 和 CTxOut 的屬性 scriptSig和scriptPubkey 就是鑰匙和鎖。scriptSig[解鎖指令碼] 就是用於對應簽名的鑰匙,而 scriptPubkey[鎖定指令碼]就是根據地址而生成的鎖。
2、資料結構
程式碼目錄:src/script/script.h
CScript 實際上就是一個vector<unsigned char>
也就是說 Script 實際上就是一串 Bytes 流。只不過這個位元組流是可以被解析為 <指令> 或者 <指令> <資料> 這樣的一個一個元資訊。而一個 Script 就是這些元資訊組成的位元組流。
其中我們需要關注的是指令:它的驗證需要一個 VM 來執行(指令碼)
/** Script opcodes */
enum opcodetype
{
OP_0 = 0x00,
OP_FALSE = OP_0,
...
OP_DEPTH = 0x74,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_NIP = 0x77,
...
OP_AND = 0x84,
OP_OR = 0x85,
OP_XOR = 0x86,
OP_EQUAL = 0x87,
...
};
3、標準交易
A. P2PKH
例如:A支付B 0.1BTC
鎖定指令碼:OP_DUP OP_HASH160 <B Public Key Hash> OP_EQUAL OP_CHECKSIG
解鎖指令碼:<B Signature> <B Public Key>
B. P2PK
例如:A支付B 0.1BTC
鎖定指令碼:<Public Key A> OP_CHECKSIG
解鎖指令碼:<Signature from Private Key A>
C. MS[多重簽名]
例如:M-N多重簽名,M是使得多重簽名生效的最少數目,如2-3:
鎖定指令碼:2 <Public Key A> <Public Key B> <Public Key C> 3 OP_CHECKMULTISIG
解鎖指令碼(3個存檔公鑰中的任意2個相一致的私鑰簽名組合予以解鎖):OP_0 <Signature B> <Signature C>
D. P2SH
P2SH是MS多重簽名的簡化版本,對鎖定指令碼進行加密,驗證過程分兩步,首先驗證的是接收方附上的贖回指令碼是否符合傳送方的鎖定指令碼,如果是,便執行該指令碼,進行多重簽名的驗證。這樣暫緩節點儲存的壓力。
鎖定指令碼(1):2 <Public Key A> <Public Key B> <Public Key C> 3 OP_CHECKMULTISIG
對於鎖定指令碼(1)首先採用SHA256雜湊演算法,隨後對其運用RIPEMD160演算法得到:
鎖定指令碼(2): OP_HASH160 8ac1d7a2fa204a16dc984fa81cfdf86a2a4e1731 OP_EQUAL
解鎖指令碼: <Sig1> <Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 OP_CHECKMULTISIG>
4、指令碼執行
我們以P2PKH交易為例,A支付B 0.1BTC,來看一下指令碼執行流程:
交易輸入,包含解鎖指令碼:<A Signature> <A Public Key>
上一筆交易的輸出,包含鎖定指令碼:OP_DUP OP_HASH160 <A Public Key Hash> OP_EQUAL OP_CHECKSIG
程式碼目錄:src/script/interpreter.cpp
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror)
{
...
// 基本資料結構:棧
std::vector<std::vector<unsigned char> > stack, stackCopy;
// 執行解鎖指令碼,執行完之後,stack中包含 <A Signature> <A Public Key>
if (!EvalScript(stack, scriptSig, flags, checker, SigVersion::BASE, serror))
return false;
// 由於下面EvalScript會破壞stack,而再後面仍然需要stack當前的資料,因而需要做一次拷貝
if (flags & SCRIPT_VERIFY_P2SH)
stackCopy = stack;
// 執行鎖定指令碼
if (!EvalScript(stack, scriptPubKey, flags, checker, SigVersion::BASE, serror))
return false;
...
}
三、指令碼建立
1. 鎖定指令碼建立
交易輸出的鎖定指令碼如何生成。生成鎖定指令碼的程式碼如下:
CScript scriptPubKey = GetScriptForDestination(destination);
程式碼目錄:src/script/standard.cpp
CScript GetScriptForDestination(const CTxDestination& dest)
{
CScript script;
boost::apply_visitor(CScriptVisitor(&script), dest);
return script;
}
// 可以看出一個標準的支付指令碼
bool operator()(const CKeyID &keyID) const {
script->clear();
*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG;
return true;
}
2. 解鎖指令碼建立
參考部落格:here
交易輸入中包含解鎖指令碼[scriptSig]:<A Signature> <A Public Key>
程式碼目錄:src/script/sign.cpp
A. ProduceSignature
/** 入參
* provider: keystore,存放了公鑰-私鑰配對
* creator:BaseSignatureCreator型別的例項,用於最後對交易生成簽名
* fromPubKey:CScript型別,交易輸入引用的UTXO的鎖定指令碼
* sigData:SignatureData型別,是輸出引數,用於存放生成的解鎖指令碼
*/
bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreator& creator, const CScript& fromPubKey, SignatureData& sigdata)
{
// 進行簽名
bool solved = SignStep(provider, creator, fromPubKey, result, whichType, SigVersion::BASE, sigdata);
bool P2SH = false;
CScript subscript;
sigdata.scriptWitness.stack.clear();
// P2SH交易,需要對子指令碼進行簽名
if (solved && whichType == TX_SCRIPTHASH) { ... }
// P2WKH交易,需要對見證指令碼簽名
if (solved && whichType == TX_WITNESS_V0_KEYHASH) { ... }
// P2WSH交易
else if (solved && whichType == TX_WITNESS_V0_SCRIPTHASH) { ... }
else if (solved && whichType == TX_WITNESS_UNKNOWN) { ... }
if (P2SH) { ... }
// 將生成的解鎖指令碼寫入到sigdata中
sigdata.scriptSig = PushAll(result);
// 校驗指令碼
sigdata.complete = solved && VerifyScript(sigdata.scriptSig, fromPubKey, &sigdata.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, creator.Checker());
return sigdata.complete;
}
B. SignStep
static bool SignStep ...
{
...
// 解析交易輸入引用的UTXO的鎖定指令碼,鎖定指令碼的型別存在輸出引數whichTypeRet中,鎖定指令碼的資料存放在向量vSolutions中
if (!Solver(scriptPubKey, whichTypeRet, vSolutions))
return false;
// 根據不同的鎖定指令碼的型別執行簽名
switch (whichTypeRet)
{
...
case TX_PUBKEY:
if (!CreateSig(creator, sigdata, provider, sig, CPubKey(vSolutions[0]).GetID(), scriptPubKey, sigversion)) return false;
ret.push_back(std::move(sig));
return true;
...
}
}
C. Solver
它根據傳入的scriptPubKey,判斷交易輸出型別(P2PKH、P2SH、P2WPKH或P2WSH),並返回相應的資料
型別 | 格式 | 返回值 |
---|---|---|
P2SH | OP_HASH160 20 [20 byte hash] OP_EQUAL | 返回20位元組的Redeem Script Hash |
P2WPKH | 隔離見證,{見證版本}{見證程式},長度20 | 返回20位元組的PubKey Hash |
P2WSH | 隔離見證,{見證版本}{見證程式},長度32 | 返回32位元組的Redeem Script Hash |
P2PK | / | 返回65/33位元組的PubKey Hash,33為壓縮版 |
P2PKH | / | 返回20位元組的PubKey Hash |
MS | {required} {A pubkey } {B pubkey} … OP_CHECKMULTISIG | 返回m PubKeys n |
D. CreateSig
bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& address, const CScript& scriptCode, SigVersion sigversion) const
{
CKey key;
if (!provider.GetKey(address, key))
return false;
// 見證指令碼必須是壓縮版
if (sigversion == SigVersion::WITNESS_V0 && !key.IsCompressed())
return false;
// 生成交易雜湊,用於簽名
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion);
// 使用ECDSA橢圓曲線加密演算法進行簽名
if (!key.Sign(hash, vchSig))
return false;
vchSig.push_back((unsigned char)nHashType);
return true;
}
四、交易建立
瞭解前面內容後,我們來看一下一個完整的交易建立,以A向B轉賬0.1BTC,採用P2PK為例:
程式碼目錄:src/wallet/rpcwallet.cpp
發起轉賬:
// 轉賬
static CTransactionRef SendMoney(CWallet * const pwallet, const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, const CCoinControl& coin_control, mapValue_t mapValue, std::string fromAccount)
{
CAmount curBalance = pwallet->GetBalance();
// 檢查金額
if (nValue <= 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid amount");
if (nValue > curBalance)
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds");
if (pwallet->GetBroadcastTransactions() && !g_connman) {
throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled");
}
// 處理目的地址,生成鎖定指令碼
CScript scriptPubKey = GetScriptForDestination(address);
// 建立交易
CReserveKey reservekey(pwallet);
CAmount nFeeRequired;
std::string strError;
std::vector<CRecipient> vecSend;
int nChangePosRet = -1;
CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount};
vecSend.push_back(recipient);
CTransactionRef tx;
if (!pwallet->CreateTransaction(vecSend, tx, reservekey, nFeeRequired, nChangePosRet, strError, coin_control)) {
if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance)
strError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired));
throw JSONRPCError(RPC_WALLET_ERROR, strError);
}
// 確認併發送交易
CValidationState state;
if (!pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */, std::move(fromAccount), reservekey, g_connman.get(), state)) {
strError = strprintf("Error: The transaction was rejected! Reason given: %s", FormatStateMessage(state));
throw JSONRPCError(RPC_WALLET_ERROR, strError);
}
return tx;
}
建立交易:
bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CReserveKey& reservekey, CAmount& nFeeRet,int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool sign)
{
...
// 金額不能為負,至少有一個收款人
for (const auto& recipient : vecSend) { ... }
CMutableTransaction txNew;
// 把nLockTime 設定為當前區塊高度, 目地是為了防止費用狙擊[fee sniping]
txNew.nLockTime = chainActive.Height();
// 偶爾允許nLockTime往前推, 因為確實有些交易會被耽誤,比如高延遲混合網路,或者為了保護隱私的壓縮交易
if (GetRandInt(10) == 0)
txNew.nLockTime = std::max(0, (int)txNew.nLockTime - GetRandInt(100));
...
/* 第一步:找到所有可用的幣 */
std::vector<COutput> vAvailableCoins;
AvailableCoins(vAvailableCoins, true, &coin_control);
// 選幣引數
CoinSelectionParams coin_selection_params;
// 改變找零地址的指令碼
CScript scriptChange;
/* 第二步:設定找零地址 */
// 已設定了找零地址:直接用
if (!boost::get<CNoDestination>(&coin_control.destChange)) {
scriptChange = GetScriptForDestination(coin_control.destChange);
}
// 未設定找零地址:生成新地址
else {
// 從鑰匙池中生成新地址
if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
strFailReason = _("Can't generate a change-address key. Private keys are disabled for this wallet.");
return false;
}
...
scriptChange = GetScriptForDestination(GetDestinationForKey(vchPubKey, change_type));
}
...
/* 第三步:計算交易需要的最低費率 */
CFeeRate nFeeRateNeeded = GetMinimumFeeRate(*this, coin_control, ::mempool, ::feeEstimator, &feeCalc);
nFeeRet = 0;
bool pick_new_inputs = true;
CAmount nValueIn = 0;
// 從零費用開始迴圈,直到手續費足夠
while (true)
{
...
/* 第四步:湊出最接近目標值(含手續費)的交易集合 */
setCoins.clear();
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
{
if (bnb_used) {
coin_selection_params.use_bnb = false;
continue;
}
else {
strFailReason = _("Insufficient funds"); // 金額不足
return false;
}
}
}
/* 第五步:構造vout, v[0]接收者,v[1]自己*/
// 把接收者加入進vout
txNew.vout.push_back(txout);
// 需要找零
if (nChange > 0)
{
// 把自己加入進vout
CTxOut newTxOut(nChange, scriptChange);
txNew.vout.insert(position, newTxOut);
} else {
nChangePosInOut = -1;
}
/* 第六步:用選出的交易,構造vin */
for (const auto& coin : setCoins) {
txNew.vin.push_back(CTxIn(coin.outpoint,CScript()));
}
/* 第七步:估算手續費 */
...
}
// 如果沒有找零,返回任何key
if (nChangePosInOut == -1) reservekey.ReturnKey();
// 打亂幣序,填充最終vin
txNew.vin.clear();
std::vector<CInputCoin> selected_coins(setCoins.begin(), setCoins.end());
std::shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext());
...
// 生成解鎖指令碼
if (!ProduceSignature(*this, MutableTransactionSignatureCreator(&txNew, nIn, coin.txout.nValue, SIGHASH_ALL), scriptPubKey, sigdata