比特幣探究之隔離見證

隔離見證(segregated witness,簡稱segwit),是比特幣歷史上一次很重要的升級,涉及到共識規(guī)則和網(wǎng)絡協(xié)議。它正式激活于2017年8月24日,區(qū)塊高度481,824。此前,比特幣的交易驗證,需要依賴兩部分數(shù)據(jù),一部分是交易狀態(tài),簡單地說就是誰給誰轉賬多少錢;另一部分是見證數(shù)據(jù),證明這個交易的真實性和合法性。我們知道,交易一旦確定,狀態(tài)就是不可更改的了,但是見證數(shù)據(jù)由于其算法設計,卻是可以改變的,或者說證據(jù)是可以不只一份的。那么如果有惡意攻擊者,通過修改見證數(shù)據(jù)就可以修改交易ID,這被稱之為延展性攻擊,會帶來相當?shù)牟话踩?。?jù)說Mt.Gox黑客事件就從這個漏洞而來。

隔離見證的提出,將見證數(shù)據(jù)隔離在區(qū)塊基本信息之外,也就意味著交易ID只跟交易狀態(tài)有關,那么交易一旦發(fā)生,任何人都無法再修改交易ID,這就順利解決了所謂的延展性攻擊。同時它帶來的另外一個好處,就是區(qū)塊容量在不需要硬分叉的前提下增大了,并且為下一步閃電網(wǎng)絡鋪平了路子。

隔離見證是比特幣歷史上的重大變革

關于隔離見證的知識,還可以參見隔離見證(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里的內容是經(jīng)過了進一步處理的,已經(jīng)不再是腳本,詳情可以參見上面列出的參考文章。

那么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ù)據(jù),每個witness都由一個var_int打頭,代表接下來的數(shù)據(jù)長度。如果某個輸入沒有見證,那么其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ù)據(jù)值是不會變的,會變的是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ù)據(jù),直接返回hash
        return SerializeHash(*this, SER_GETHASH, 0);
    }
    //...
}

SerializeHash函數(shù),采用輸入流的方式讀取Transaction數(shù)據(jù),最后調用的是SerializeTransaction函數(shù):

template<typename Stream, typename TxType>
inline void SerializeTransaction(const TxType& tx, Stream& s) {
    //根據(jù)Computer時設定的參數(shù),確定帶不帶見證
    const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
    s << tx.nVersion;
    unsigned char flags = 0;
    if (fAllowWitness) {
        if (tx.HasWitness()) {  //帶見證,且確實包含見證數(shù)據(jù)
            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ù)據(jù)
        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ù)據(jù)。如果沒有見證,這里就是正常的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剛剛已經(jīng)讀了,這里只讀vout就可以了
    }
    if ((flags & 1) && fAllowWitness) {
        flags ^= 1;
        for (size_t i = 0; i < tx.vin.size(); i++) {
            s >> tx.vin[i].scriptWitness.stack;  //依次讀入見證數(shù)據(jù)
        }
    }    if (flags) {
        //如果讀入flags不是1(可能是未來版本生成的),拋出異常
        throw std::ios_base::failure("Unknown transaction optional data");
    }
    s >> tx.nLockTime;
}

三、Coinbase Commitment

我們知道,交易信息是被打包進MerkleTreeRoot,然后寫進區(qū)塊頭確保不可篡改的。那么隔離見證之后,我們同樣也要確保witness數(shù)據(jù)不可篡改。比特幣是怎么來實現(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ù)據(jù)準備好了,下面是正式處理過程,可以看出其復雜度明顯降低
        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經(jīng)雙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經(jīng)過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ù)據(jù)不允許溢出
    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ù)據(jù)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)不易,懇請支持!你的贊賞,我的動力!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容