隔離見證(segregated witness脆诉,簡稱segwit)宠纯,是比特幣歷史上一次很重要的升級,涉及到共識規(guī)則和網絡協(xié)議逝慧。它正式激活于2017年8月24日,區(qū)塊高度481,824瑟捣。此前馋艺,比特幣的交易驗證,需要依賴兩部分數(shù)據迈套,一部分是交易狀態(tài)捐祠,簡單地說就是誰給誰轉賬多少錢;另一部分是見證數(shù)據桑李,證明這個交易的真實性和合法性踱蛀。我們知道,交易一旦確定贵白,狀態(tài)就是不可更改的了率拒,但是見證數(shù)據由于其算法設計,卻是可以改變的禁荒,或者說證據是可以不只一份的猬膨。那么如果有惡意攻擊者,通過修改見證數(shù)據就可以修改交易ID呛伴,這被稱之為延展性攻擊勃痴,會帶來相當?shù)牟话踩在怂fMt.Gox黑客事件就從這個漏洞而來。
隔離見證的提出沛申,將見證數(shù)據隔離在區(qū)塊基本信息之外劣领,也就意味著交易ID只跟交易狀態(tài)有關,那么交易一旦發(fā)生铁材,任何人都無法再修改交易ID尖淘,這就順利解決了所謂的延展性攻擊。同時它帶來的另外一個好處著觉,就是區(qū)塊容量在不需要硬分叉的前提下增大了村生,并且為下一步閃電網絡鋪平了路子。
關于隔離見證的知識固惯,還可以參見隔離見證(CSDN)以及什么是隔離見證(知乎)這兩篇貼子梆造,英文好的可以直接看Github上的說明缴守。本文的重點葬毫,依然是直接切入源碼看實現(xiàn)。
那么隔離見證是如何實現(xiàn)的呢屡穗?
一贴捡、怎么隔離,在哪隔離
首先來看交易輸入CTxIn村砂,下面是它的部分代碼(src/primitives/transaction.h):
class CTxIn
{
public:
COutPoint prevout;
CScript scriptSig;
uint32_t nSequence;
CScriptWitness scriptWitness; //僅當交易被序列化時才參與
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(prevout);
READWRITE(scriptSig);
READWRITE(nSequence);
}
//...
}
簡單地理解烂斋,所謂隔離見證,就是把原來scriptSig里的主要內容础废,轉移到scriptWitness中去汛骂,注意上面的序列化代碼中,scriptWitness是不會被序列化的评腺,它只在整個交易被序列化時才參與帘瞭。同時,相應的scriptSig就變成空腳本了蒿讥,這就是所謂的隔離蝶念,附帶的一個好處就是交易size減小了,相應的交易費用也會降低芋绸。需要注意的是媒殉,scriptWitness里的內容是經過了進一步處理的,已經不再是腳本摔敛,詳情可以參見上面列出的參考文章廷蓉。
那么scriptWitness是什么時候生成的?答案是在CreateTransaction里马昙,最后生成簽名的時候桃犬。以下是src/script/sign.cpp中ProduceSignature函數(shù)的部分代碼售貌,這里只引用隔離見證相關的部分:
bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreator& creator,
const CScript& fromPubKey, SignatureData& sigdata)
{
//...
if (solved && whichType == TX_WITNESS_V0_KEYHASH)
{
CScript witnessscript; //簽名腳本
witnessscript << OP_DUP << OP_HASH160 << ToByteVector(result[0]) << OP_EQUALVERIFY << OP_CHECKSIG;
txnouttype subType;
solved = solved && SignStep(provider, creator, witnessscript, result, subType,
SigVersion::WITNESS_V0, sigdata);
sigdata.scriptWitness.stack = result; //填入scriptWitness
sigdata.witness = true;
result.clear(); //注意這里清空了
}
else if (solved && whichType == TX_WITNESS_V0_SCRIPTHASH)
{
CScript witnessscript(result[0].begin(), result[0].end());
sigdata.witness_script = witnessscript; //贖回腳本
txnouttype subType;
solved = solved && SignStep(provider, creator, witnessscript, result, subType, SigVersion::WITNESS_V0, sigdata)
&& subType != TX_SCRIPTHASH && subType != TX_WITNESS_V0_SCRIPTHASH && subType != TX_WITNESS_V0_KEYHASH;
result.push_back(std::vector<unsigned char>(witnessscript.begin(), witnessscript.end()));
sigdata.scriptWitness.stack = result; //填入scriptWitness
sigdata.witness = true;
result.clear(); //注意這里清空了
} else if (solved && whichType == TX_WITNESS_UNKNOWN) {
sigdata.witness = true;
}
sigdata.scriptSig = PushAll(result); //實際上是scriptSig清空了
//...
return sigdata.complete;
}
可以看到,如果使用了隔離見證疫萤,那么交易簽名被存入了scriptWitness颂跨,而不是scriptSig。這就是所謂隔離的由來扯饶。
注意scriptWitness內部使用的stack來存儲數(shù)據恒削,每個witness都由一個var_int打頭,代表接下來的數(shù)據長度尾序。如果某個輸入沒有見證钓丰,那么其witness就是一個0x00。
二每币、Transaction ID
一個交易的txid是以下序列的雙SHA256加密結果:
[nVersion][txins][txouts][nLockTime]
采用隔離見證以后携丁,txid的定義仍然保持不變,但是另外增加了一個wtxid兰怠,它對應的序列是這樣:
[nVersion][marker][flag][txins][txouts][witness][nLockTime]
下面是src/primitives/transaction.h(cpp)中的相關源碼梦鉴,為便于閱讀,稍有整理:
class CTransaction
{
//...
private:
//這兩個hash值在交易被構建時計算揭保,并且只在內存中不寫磁盤
//注意CTransaction數(shù)據值是不會變的肥橙,會變的是CMutableTransaction
const uint256 hash;
const uint256 m_witness_hash;
uint256 ComputeHash() const { //計算txid,注意設定了無見證參數(shù)
return SerializeHash(*this, SER_GETHASH, SERIALIZE_TRANSACTION_NO_WITNESS);
}
uint256 ComputeWitnessHash() const { //計算wtxid秸侣,第3個參數(shù)為0默認有見證
if (!HasWitness()) return hash; //如果沒有見證數(shù)據存筏,直接返回hash
return SerializeHash(*this, SER_GETHASH, 0);
}
//...
}
SerializeHash函數(shù),采用輸入流的方式讀取Transaction數(shù)據味榛,最后調用的是SerializeTransaction函數(shù):
template<typename Stream, typename TxType>
inline void SerializeTransaction(const TxType& tx, Stream& s) {
//根據Computer時設定的參數(shù)椭坚,確定帶不帶見證
const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
s << tx.nVersion;
unsigned char flags = 0;
if (fAllowWitness) {
if (tx.HasWitness()) { //帶見證,且確實包含見證數(shù)據
flags |= 1;
}
}
if (flags) {
std::vector<CTxIn> vinDummy;
s << vinDummy; //輸入一個空vector搏色,其實就是輸入一個0善茎,它對應的就是marker
s << flags; //對應flag,一定是1
}
s << tx.vin;
s << tx.vout;
if (flags & 1) { //如果帶見證继榆,依次輸入見證數(shù)據
for (size_t i = 0; i < tx.vin.size(); i++) {
s << tx.vin[i].scriptWitness.stack;
}
}
s << tx.nLockTime;
}
下面是對應的UnsierializeTransaction函數(shù):
template<typename Stream, typename TxType>
inline void UnserializeTransaction(TxType& tx, Stream& s) {
const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
s >> tx.nVersion;
unsigned char flags = 0;
tx.vin.clear();
tx.vout.clear();
s >> tx.vin; //先讀一個vin巾表,來判斷到底有沒有見證數(shù)據。如果沒有見證略吨,這里就是正常的vin
if (tx.vin.size() == 0 && fAllowWitness) { //確實是空的集币,而且?guī)б娮C,那么剛剛讀取的就是marker
s >> flags; //再讀入flag翠忠,目前必定為1
if (flags != 0) { //然后開始讀輸入鞠苟、輸出
s >> tx.vin;
s >> tx.vout;
}
} else {
s >> tx.vout; //vin剛剛已經讀了,這里只讀vout就可以了
}
if ((flags & 1) && fAllowWitness) {
flags ^= 1;
for (size_t i = 0; i < tx.vin.size(); i++) {
s >> tx.vin[i].scriptWitness.stack; //依次讀入見證數(shù)據
}
} if (flags) {
//如果讀入flags不是1(可能是未來版本生成的),拋出異常
throw std::ios_base::failure("Unknown transaction optional data");
}
s >> tx.nLockTime;
}
三当娱、Coinbase Commitment
我們知道吃既,交易信息是被打包進MerkleTreeRoot,然后寫進區(qū)塊頭確保不可篡改的跨细。那么隔離見證之后鹦倚,我們同樣也要確保witness數(shù)據不可篡改。比特幣是怎么來實現(xiàn)的呢冀惭?
首先震叙,所有的wtxid會被打包進見證版的Merkle樹,見src/consensus/merkle.cpp中的BlockWitnessMerkleRoot函數(shù):
uint256 BlockWitnessMerkleRoot(const CBlock& block, bool* mutated)
{
std::vector<uint256> leaves;
leaves.resize(block.vtx.size());
leaves[0].SetNull(); //幣基交易的見證哈希是0.
for (size_t s = 1; s < block.vtx.size(); s++) {
leaves[s] = block.vtx[s]->GetWitnessHash();
}
return ComputeMerkleRoot(std::move(leaves), mutated);
}
隨后散休,在生成區(qū)塊的時候媒楼,創(chuàng)建幣基交易時,會生成一個Coinbase Commitment(幣基承諾)戚丸。下面是src/miner.cpp中CreateNewBlock函數(shù)的節(jié)選:
CMutableTransaction coinbaseTx;
coinbaseTx.vin.resize(1);
coinbaseTx.vin[0].prevout.SetNull();
coinbaseTx.vout.resize(1);
coinbaseTx.vout[0].scriptPubKey = scriptPubKeyIn;
coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus());
coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0;
pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx));
pblocktemplate->vchCoinbaseCommitment = GenerateCoinbaseCommitment(*pblock, pindexPrev, chainparams.GetConsensus());
pblocktemplate->vTxFees[0] = -nFees;
上面倒數(shù)第二行划址,調用了GenerateCoinbaseCommitment函數(shù),它定義在src/validation.cpp中限府,源碼是這樣的:
std::vector<unsigned char> GenerateCoinbaseCommitment(CBlock& block, const CBlockIndex* pindexPrev,
const Consensus::Params& consensusParams)
{
std::vector<unsigned char> commitment;
int commitpos = GetWitnessCommitmentIndex(block); //從幣基交易的輸出中尋找承諾項夺颤,沒找到就返回-1
std::vector<unsigned char> ret(32, 0x00);
if (consensusParams.vDeployments[Consensus::DEPLOYMENT_SEGWIT].nTimeout != 0) {
if (commitpos == -1) { //沒有找到,就開始創(chuàng)建承諾谣殊,先生成見證版Merkle樹根
uint256 witnessroot = BlockWitnessMerkleRoot(block, nullptr);
CHash256().Write(witnessroot.begin(), 32).Write(ret.data(), 32).Finalize(witnessroot.begin());
CTxOut out; //構建一個幣基交易的輸出
out.nValue = 0; //金額是0
out.scriptPubKey.resize(38); //公鑰腳本長度38拂共,前6個字節(jié)固定為0x6a24aa21a9ed
out.scriptPubKey[0] = OP_RETURN; //0x6a
out.scriptPubKey[1] = 0x24; //36,即后面的總長度
out.scriptPubKey[2] = 0xaa; //0xaa21a9ed姻几,固定不變的承諾頭
out.scriptPubKey[3] = 0x21;
out.scriptPubKey[4] = 0xa9;
out.scriptPubKey[5] = 0xed;
memcpy(&out.scriptPubKey[6], witnessroot.begin(), 32); //插入見證版Merkle樹根
commitment = std::vector<unsigned char>(out.scriptPubKey.begin(), out.scriptPubKey.end());
CMutableTransaction tx(*block.vtx[0]);
tx.vout.push_back(out); //幣基交易中添加這個輸出
block.vtx[0] = MakeTransactionRef(std::move(tx)); //寫回區(qū)塊
}
}
UpdateUncommittedBlockStructures(block, pindexPrev, consensusParams); //更新區(qū)塊其他結構
return commitment;
}
幣基交易中添加輸出之后,它的輸入也有相應變化势告,也就是上面最后調用的UpdateUncommittedBlockStructures函數(shù):
void UpdateUncommittedBlockStructures(CBlock& block, const CBlockIndex* pindexPrev,
const Consensus::Params& consensusParams)
{
int commitpos = GetWitnessCommitmentIndex(block);
static const std::vector<unsigned char> nonce(32, 0x00);
if (commitpos != -1 && IsWitnessEnabled(pindexPrev, consensusParams) && !block.vtx[0]->HasWitness()) {
CMutableTransaction tx(*block.vtx[0]); //修改幣基交易
tx.vin[0].scriptWitness.stack.resize(1); //向空輸入中添加一項
tx.vin[0].scriptWitness.stack[0] = nonce;
block.vtx[0] = MakeTransactionRef(std::move(tx)); //寫回區(qū)塊
}
}
OK蛇捌,既然費那么大勁寫入承諾,那么一定要對它進行檢查咱台,否則就失去意義了络拌。這段代碼在ContextualCheckBlock函數(shù)中,以下是它的部分代碼:
bool fHaveWitness = false;
if (VersionBitsState(pindexPrev, consensusParams, Consensus::DEPLOYMENT_SEGWIT, versionbitscache)
== ThresholdState::ACTIVE) {
int commitpos = GetWitnessCommitmentIndex(block);
if (commitpos != -1) {
bool malleated = false;
uint256 hashWitness = BlockWitnessMerkleRoot(block, &malleated);
if (block.vtx[0]->vin[0].scriptWitness.stack.size() != 1
|| block.vtx[0]->vin[0].scriptWitness.stack[0].size() != 32) {
return state.DoS(100, false, REJECT_INVALID, "bad-witness-nonce-size", true,
strprintf("%s : invalid witness reserved value size", __func__));
}
CHash256().Write(hashWitness.begin(), 32)
.Write(&block.vtx[0]->vin[0].scriptWitness.stack[0][0], 32)
.Finalize(hashWitness.begin());
if (memcmp(hashWitness.begin(), &block.vtx[0]->vout[commitpos].scriptPubKey[6], 32)) {
return state.DoS(100, false, REJECT_INVALID, "bad-witness-merkle-match", true,
strprintf("%s : witness merkle commitment mismatch", __func__));
}
fHaveWitness = true;
}
}
四回溺、交易哈希算法
隔離見證同時還修改了交易簽名所用的哈希算法春贸,此前原有算法存在兩個方面缺陷,一個是當交易中sigOp數(shù)量增加時遗遵,復雜度呈平方增長萍恕;另一個是算法不涉及輸入金額,可能對冷錢包的使用有所影響车要。
關于新的交易哈希算法的詳細解釋允粤,可以參見Github上的原文 。下面直接摘取src/script/interpreter.cpp中的SignatureHash函數(shù)的部分源碼:
template <class T>
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int nHashType,
const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache)
{
//...
if (sigversion == SigVersion::WITNESS_V0) {
uint256 hashPrevouts, hashSequence, hashOutputs;
const bool cacheready = cache && cache->ready;
if (!(nHashType & SIGHASH_ANYONECANPAY)) {
hashPrevouts = cacheready ? cache->hashPrevouts : GetPrevoutHash(txTo);
}
if (!(nHashType & SIGHASH_ANYONECANPAY) && (nHashType & 0x1f) != SIGHASH_SINGLE
&& (nHashType & 0x1f) != SIGHASH_NONE) {
hashSequence = cacheready ? cache->hashSequence : GetSequenceHash(txTo);
}
if ((nHashType & 0x1f) != SIGHASH_SINGLE && (nHashType & 0x1f) != SIGHASH_NONE) {
hashOutputs = cacheready ? cache->hashOutputs : GetOutputsHash(txTo);
} else if ((nHashType & 0x1f) == SIGHASH_SINGLE && nIn < txTo.vout.size()) {
CHashWriter ss(SER_GETHASH, 0);
ss << txTo.vout[nIn];
hashOutputs = ss.GetHash();
}
//數(shù)據準備好了,下面是正式處理過程类垫,可以看出其復雜度明顯降低
CHashWriter ss(SER_GETHASH, 0);
ss << txTo.nVersion; //版本號
ss << hashPrevouts;
ss << hashSequence;
ss << txTo.vin[nIn].prevout;
ss << scriptCode;
ss << amount; //金額這里包含了
ss << txTo.vin[nIn].nSequence;
ss << hashOutputs;
ss << txTo.nLockTime;
ss << nHashType;
return ss.GetHash();
}
//...
}
五司光、腳本驗證
在創(chuàng)建交易的最后,會對簽名腳本進行驗證悉患,涉及到隔離見證的部分残家,先看src/scripts/interpreter.cpp中VerifyScript函數(shù)的部分源碼:
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness,
unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror)
{
//...
int witnessversion;
std::vector<unsigned char> witnessprogram;
if (flags & SCRIPT_VERIFY_WITNESS) {
if (scriptPubKey.IsWitnessProgram(witnessversion, witnessprogram)) {
hadWitness = true;
if (scriptSig.size() != 0) {
return set_error(serror, SCRIPT_ERR_WITNESS_MALLEATED);
}
if (!VerifyWitnessProgram(*witness, witnessversion, witnessprogram, flags, checker, serror)) {
return false;
}
stack.resize(1);
}
}
//...
}
可以看到,它調用了VerifyWitnessProgram來進行驗證售躁。它的源碼是這樣的:
static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
const std::vector<unsigned char>& program, unsigned int flags,
const BaseSignatureChecker& checker, ScriptError* serror)
{
std::vector<std::vector<unsigned char> > stack;
CScript scriptPubKey;
if (witversion == 0) {
if (program.size() == WITNESS_V0_SCRIPTHASH_SIZE) {
//32位的P2WSH跪削,witness為stack + witnessScript,而witnessScript經雙SHA256就是32位program
if (witness.stack.size() == 0) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY);
}
scriptPubKey = CScript(witness.stack.back().begin(), witness.stack.back().end());
stack = std::vector<std::vector<unsigned char> >(witness.stack.begin(), witness.stack.end() - 1);
uint256 hashScriptPubKey;
CSHA256().Write(&scriptPubKey[0], scriptPubKey.size()).Finalize(hashScriptPubKey.begin());
if (memcmp(hashScriptPubKey.begin(), program.data(), 32)) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH);
}
} else if (program.size() == WITNESS_V0_KEYHASH_SIZE) {
//20位P2WPKH迂求,witness就是sig + pubkey碾盐,其中pubkey經過HASH160之后就是20位program
if (witness.stack.size() != 2) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH);
}
scriptPubKey << OP_DUP << OP_HASH160 << program << OP_EQUALVERIFY << OP_CHECKSIG;
stack = witness.stack;
} else {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WRONG_LENGTH);
}
} else if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM);
} else { //高版本見證腳本就等將來的軟分叉吧
return set_success(serror);
}
//棧數(shù)據不允許溢出
for (unsigned int i = 0; i < stack.size(); i++) {
if (stack.at(i).size() > MAX_SCRIPT_ELEMENT_SIZE)
return set_error(serror, SCRIPT_ERR_PUSH_SIZE);
}
//執(zhí)行一下,看看結果是不是TRUE
if (!EvalScript(stack, scriptPubKey, flags, checker, SigVersion::WITNESS_V0, serror)) {
return false;
}
//stack最后只能剩1個數(shù)據TRUE
if (stack.size() != 1)
return set_error(serror, SCRIPT_ERR_CLEANSTACK);
if (!CastToBool(stack.back()))
return set_error(serror, SCRIPT_ERR_EVAL_FALSE);
return true;
}
謝謝閱讀揩局。如有不妥之處毫玖,請高手不吝指正。
原創(chuàng)不易凌盯,懇請支持付枫!你的贊賞,我的動力驰怎!