为了账号安全,请及时绑定邮箱和手机立即绑定

比特币探究之隔离见证

标签:
区块链

隔离见证(segregated witness,简称segwit),是比特币历史上一次很重要的升级,涉及到共识规则和网络协议。它正式激活于2017年8月24日,区块高度481,824。此前,比特币的交易验证,需要依赖两部分数据,一部分是交易状态,简单地说就是谁给谁转账多少钱;另一部分是见证数据,证明这个交易的真实性和合法性。我们知道,交易一旦确定,状态就是不可更改的了,但是见证数据由于其算法设计,却是可以改变的,或者说证据是可以不只一份的。那么如果有恶意攻击者,通过修改见证数据就可以修改交易ID,这被称之为延展性攻击,会带来相当的不安全性。据说Mt.Gox黑客事件就从这个漏洞而来。

隔离见证的提出,将见证数据隔离在区块基本信息之外,也就意味着交易ID只跟交易状态有关,那么交易一旦发生,任何人都无法再修改交易ID,这就顺利解决了所谓的延展性攻击。同时它带来的另外一个好处,就是区块容量在不需要硬分叉的前提下增大了,并且为下一步闪电网络铺平了路子。

隔离见证是比特币历史上的重大变革

关于隔离见证的知识,还可以参见隔离见证(CSDN)以及什么是隔离见证(知乎)这两篇贴子,英文好的可以直接看Github上的说明。本文的重点,依然是直接切入源码看实现。

那么隔离见证是如何实现的呢?

一、怎么隔离,在哪隔离

首先来看交易输入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函数的部分代码,这里只引用隔离见证相关的部分:

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来存储数据,每个witness都由一个var_int打头,代表接下来的数据长度。如果某个输入没有见证,那么其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数据值是不会变的,会变的是CMutableTransaction
    const uint256 hash;    const uint256 m_witness_hash;    uint256 ComputeHash() const {  //计算txid,注意设定了无见证参数
        return SerializeHash(*this, SER_GETHASH, SERIALIZE_TRANSACTION_NO_WITNESS);
    }    uint256 ComputeWitnessHash() const {  //计算wtxid,第3个参数为0默认有见证
        if (!HasWitness())  return hash;  //如果没有见证数据,直接返回hash
        return SerializeHash(*this, SER_GETHASH, 0);
    }    //...}

SerializeHash函数,采用输入流的方式读取Transaction数据,最后调用的是SerializeTransaction函数:

template<typename Stream, typename TxType>inline void SerializeTransaction(const TxType& tx, Stream& s) {    //根据Computer时设定的参数,确定带不带见证
    const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
    s << tx.nVersion;    unsigned char flags = 0;    if (fAllowWitness) {        if (tx.HasWitness()) {  //带见证,且确实包含见证数据
            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) {  //如果带见证,依次输入见证数据
        for (size_t i = 0; i < tx.vin.size(); i++) {
            s << tx.vin[i].scriptWitness.stack;
        }
    }
    s << tx.nLockTime;
}

下面是对应的UnsierializeTransaction函数:

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,来判断到底有没有见证数据。如果没有见证,这里就是正常的vin
    if (tx.vin.size() == 0 && fAllowWitness) {  //确实是空的,而且带见证,那么刚刚读取的就是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;  //依次读入见证数据
        }
    }    if (flags) {        //如果读入flags不是1(可能是未来版本生成的),抛出异常
        throw std::ios_base::failure("Unknown transaction optional data");
    }
    s >> tx.nLockTime;
}

三、Coinbase Commitment

我们知道,交易信息是被打包进MerkleTreeRoot,然后写进区块头确保不可篡改的。那么隔离见证之后,我们同样也要确保witness数据不可篡改。比特币是怎么来实现的呢?

首先,所有的wtxid会被打包进见证版的Merkle树,见src/consensus/merkle.cpp中的BlockWitnessMerkleRoot函数:

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);
}

随后,在生成区块的时候,创建币基交易时,会生成一个Coinbase Commitment(币基承诺)。下面是src/miner.cpp中CreateNewBlock函数的节选:

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;

上面倒数第二行,调用了GenerateCoinbaseCommitment函数,它定义在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) {  //没有找到,就开始创建承诺,先生成见证版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个字节固定为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));  //写回区块
        }
    }
    UpdateUncommittedBlockStructures(block, pindexPrev, consensusParams);  //更新区块其他结构
    return commitment;
}

币基交易中添加输出之后,它的输入也有相应变化,也就是上面最后调用的UpdateUncommittedBlockStructures函数:

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));  //写回区块
    }
}

OK,既然费那么大劲写入承诺,那么一定要对它进行检查,否则就失去意义了。这段代码在ContextualCheckBlock函数中,以下是它的部分代码:

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数量增加时,复杂度呈平方增长;另一个是算法不涉及输入金额,可能对冷钱包的使用有所影响。

关于新的交易哈希算法的详细解释,可以参见Github上的原文 。下面直接摘取src/script/interpreter.cpp中的SignatureHash函数的部分源码:

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();
        }        //数据准备好了,下面是正式处理过程,可以看出其复杂度明显降低
        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();
    }    //...}

五、脚本验证

在创建交易的最后,会对签名脚本进行验证,涉及到隔离见证的部分,先看src/scripts/interpreter.cpp中VerifyScript函数的部分源码:

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);
    }    //栈数据不允许溢出
    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);
    }    //执行一下,看看结果是不是TRUE
    if (!EvalScript(stack, scriptPubKey, flags, checker, SigVersion::WITNESS_V0, serror)) {        return false;
    }    //stack最后只能剩1个数据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;
}

谢谢阅读。如有不妥之处,请高手不吝指正。


原创不易,恳请支持!你的赞赏,我的动力!



作者:魏兆华
原文链接:https://www.jianshu.com/p/eef8e260b9f7


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消