二十九.签名与脚本(4)--脚本验证例子

1.整理思路

接下来,我们来写一个例子,构建一个测试的tx,然后调用VerifySignature函数,来验证一下整个流程。

我们需要准备些什么?一些账号。

一个用来引用的tx,在那个tx里,我们给自己账号0.5个btc奖励(或来源挖矿coinbase)

然后构建交易转给某人一点btc,生成签名。

2.生成签名

生成签名我们需要调用SignSignature函数,我们来看一下这个函数的参数:

cpp 复制代码
bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, 
int nHashType=SIGHASH_ALL, CScript scriptPrereq=CScript());

后两个参数默认,所以我们需要提供前三个参数。

我们来看一下源码中是怎样调用这个函数的:

cpp 复制代码
        // Sign
        int nIn = 0;
        foreach(CWalletTx* pcoin, setCoins)
            for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
                if (pcoin->vout[nOut].IsMine())
                    SignSignature(*pcoin, wtxNew, nIn++);

它需要遍历pcoin这个tx下的vout,然后判断属于自己的,才调用SignSignature,进行签名。

而我们这里,由于自己构建的引用tx,预先知道都是自己的vout,所以不用这些判断。

并且构建的转账tx,我们只使用一个vin。

3.引用第几个vout

但是这里有个问题,此函数的第一个参数有了,*pcoin就是引用的tx,nIn指明对第几个vin进行签名。

wtxNew是转账要构建的tx。

上面的意思是将wtxNew下的第nIn个vin,进行签名,而这个vin要引用的vout就在*pcoin内。

但是这里还缺少一个东西啊,你引用的vout,是第几个呢?

我们来看SignSignature函数内部:

cpp 复制代码
bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq)
{
    assert(nIn < txTo.vin.size());
    CTxIn& txin = txTo.vin[nIn];
    assert(txin.prevout.n < txFrom.vout.size());
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    // Leave out the signature from the hash, since a signature can't sign itself.
    // The checksig op will also drop the signatures from its hash.
    uint256 hash = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);

    if (!Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig))
        return false;

    txin.scriptSig = scriptPrereq + txin.scriptSig;

    // Test solution
    if (scriptPrereq.empty())
        if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn))
            return false;

    return true;
}

看到没有,引用的CTxOut,根据txin.prevout.n得来的,说明这个vin引用的第几个vout,在wtxNew已经提前构建好了。不需要指明,所以这里我们写例子的时候也需要提前构建好。

但是这里还有个可能是漏洞,就是:

cpp 复制代码
    if (pcoin->vout[nOut].IsMine())
                    SignSignature(*pcoin, wtxNew, nIn++);

你怎么就能保证,当前的voutnout.IsMine,就和wteNew下的txFrom.vouttxin.prevout.n;是对应的呢(如果你不显式指定传入nOut),是因为之前添加的时候,也遵循着同样的添加原则,如下:

cpp 复制代码
foreach(CWalletTx* pcoin, setCoins)
    for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
        if (pcoin->vout[nOut].IsMine())
            wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut));

一次IsMine添加一次。所以是对应的,如果还不懂,我可以写这样一个例子:

cpp 复制代码
int main()
{
	int a[10];
	int j = 0;
	for (int i = 0; i < 10; i++)
	{
		if (i % 2 == 0) {  //赋值 相当于依次存入vout
			a[j] = i;
			j++;
		}

  }
	j = 0;
	for (int i = 0; i < 10; i++)
	{
		if (i % 2 == 0) {
			if (a[j] == i) {//依次取出是否对应
				cout << "true!" << endl;
				j++;
			}
		}
	}
}

必定是输出五个true,因为遵循着相同的循环原则,是对应的。

当然定上面是有助于我们理解源码,如果我们来写例子的话,是用不上的。

我们只需要,提前构建好*pcoin和wtxNew(硬写入),直接调用函数就行了。不需要加那么多判断。

4.构建*pcoin

构建引用的tx,我这里单独定了一个函数,如下:

cpp 复制代码
CTransaction CreateTxPrev()
{
    // 假设这是上一笔交易(Coinbase 或别人转给你的交易)
    CTransaction txPrev;
    // 设置一个vout
    txPrev.vout.resize(1);

    // 转账给自己0.5btc  按实际"聪"为单位
    txPrev.vout[0].nValue = 50000000;   // 0.5 BTC

    // scriptPubKey: 标准的 P2PKH
    CScript scriptPubKey;
    scriptPubKey << OP_DUP << OP_HASH160 << Hash160(myPubKey) << OP_EQUALVERIFY << OP_CHECKSIG;

    txPrev.vout[0].scriptPubKey = scriptPubKey;
    
    return txPrev;
}

但是这里有些问题,构建锁定脚本时,需要一个hash160格式的账号,即Hash160(myPubKey)这里,并且这个账号得是属于你的,否则后面签名将无法成功。无法引用该vout。

5.获得账号

我们回顾一下之前的章节,账号我们可以自己创建,也可以从钱包里获取。

我们调用LoadWallet函数来获得账号,这个函数会加载默认账号,如果没有则创建,我们可以看一下这个函数的代码:

cpp 复制代码
bool LoadWallet()
{
    vector<unsigned char> vchDefaultKey;
    if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
        return false;

    if (mapKeys.count(vchDefaultKey))
    {
        // Set keyUser
        keyUser.SetPubKey(vchDefaultKey);
        keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
    }
    else
    {
        // Create new keyUser and set as default key
        keyUser.MakeNewKey();
        if (!AddKey(keyUser))
            return false;
        if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
            return false;
        CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
    }

    return true;
}

它会把这个账号存到keyUser里,这是个CKey类型。

另外在这个函数里,调用MakeNewKey就是创建账号了,到时候我们的转账的收款人,就用这个函数临时创建账号。

6.获取65字节公钥

由于keyUser是CKey类型,而我们的脚本需要的是hash160的账号。

我们可以调用Hash160函数将公钥转换成160哈希值,但是这个函数接收的是65字节的公钥。

所以我们还需要从keyUser里获取到65字节的公钥,调用GetPubKey即可获取。

用代码表示很简单,就是下面这句:

cpp 复制代码
scriptPubKey << OP_DUP << OP_HASH160 << Hash160(keyUser.GetPubKey()) << OP_EQUALVERIFY << OP_CHECKSIG;

将CreateTxPrev函数里锁定脚本改为上面语句即可。

7.构建新交易

接下来我们来写个构建新交易的函数,跟CreateTxPrev原理差不多,就是多了vin的加入。

因为签名引用的时候不需要用到vin部分,所以在CreateTxPrev里就把这部分省略了。

但新构建的必须要有用到,函数代码如下:

cpp 复制代码
//创建转账tx  即建立一笔新交易
CTransaction CreateTxNew(uint256 prevTxHash,CKey &newKey)
{
    CTransaction txNew;
    txNew.SetNull();//清空一下数据

    // 构建新交易输入部分(vin) ---
    CTxIn txin;
    txin.prevout.hash = prevTxHash;  //引用coin的哈希
    txin.prevout.n = 0;              // coin的第0个输出
    txNew.vin.push_back(txin);    //添加进去

   //构建vout部分
    txNew.vout.resize(1);

    // 转账别人0.4btc
    txNew.vout[0].nValue = 40000000;   // 0.4 BTC  这里还要构建一个vout找零给自己因为引用的是0.5btc,为了简便就不写了

    // scriptPubKey: 标准的 P2PKH
    CScript scriptPubKey;
    scriptPubKey << OP_DUP << OP_HASH160 << Hash160(newKey.GetPubKey()) << OP_EQUALVERIFY << OP_CHECKSIG;

    txNew.vout[0].scriptPubKey = scriptPubKey;

    return txNew;
}

8.调用函数签名

好,有了上面两个函数后,我们就可以在主函数里来使用了,完成我们的例子,完整代码如下:

cpp 复制代码
//创建引用tx
CTransaction CreateTxPrev()
{
    // 假设这是上一笔交易(Coinbase 或别人转给你的交易)
    CTransaction txPrev;
    // 设置一个vout
    txPrev.vout.resize(1);

    // 转账给自己0.5btc  按实际"聪"为单位
    txPrev.vout[0].nValue = 50000000;   // 0.5 BTC

    // scriptPubKey: 标准的 P2PKH
    CScript scriptPubKey;
    scriptPubKey << OP_DUP << OP_HASH160 << Hash160(keyUser.GetPubKey()) << OP_EQUALVERIFY << OP_CHECKSIG;

    txPrev.vout[0].scriptPubKey = scriptPubKey;
    
    return txPrev;
}
//创建转账tx  即建立一笔新交易
CTransaction CreateTxNew(uint256 prevTxHash,CKey &newKey)
{
    CTransaction txNew;
    txNew.SetNull();//清空一下数据

    // 构建新交易输入部分(vin) ---
    CTxIn txin;
    txin.prevout.hash = prevTxHash;  //引用coin的哈希
    txin.prevout.n = 0;              // coin的第0个输出
    txNew.vin.push_back(txin);    //添加进去

   //构建vout部分
    txNew.vout.resize(1);

    // 转账别人0.4btc
    txNew.vout[0].nValue = 40000000;   // 0.4 BTC  这里还要构建一个vout找零给自己因为引用的是0.5btc,为了简便就不写了

    // scriptPubKey: 标准的 P2PKH
    CScript scriptPubKey;
    scriptPubKey << OP_DUP << OP_HASH160 << Hash160(newKey.GetPubKey()) << OP_EQUALVERIFY << OP_CHECKSIG;

    txNew.vout[0].scriptPubKey = scriptPubKey;

    return txNew;
}

int main()
{
    LoadWallet();
    CTransaction coin = CreateTxPrev();  //构建引用的coin
    CKey newKey;//准备容器创建一个新账号,用来当收款人
    newKey.MakeNewKey();//创建公私钥
    CTransaction txNew = CreateTxNew(coin.GetHash(), newKey);
    //此时newTx还没构建完成,还需要添加签名,就是我们的签名验证例子
     
    if (SignSignature(coin, txNew, 0))      // nIn = 0
    {
        printf("SignSignature 成功!\n");
    }
}

运行结果:

签名成功,这里的签名成功,也代表验证成功了,因为在SignSignature里它会测试验证一下,代码如下:

cpp 复制代码
 // Test solution
    if (scriptPrereq.empty())
        if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn))
            return false;

但我们为了直观,我们接下来,单独验证一下。

9.验证签名

我们用源码中的VerifySignature函数来验证,看一下代码:

cpp 复制代码
bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    assert(nIn < txTo.vin.size());
    const CTxIn& txin = txTo.vin[nIn];
    if (txin.prevout.n >= txFrom.vout.size())
        return false;
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    if (txin.prevout.hash != txFrom.GetHash())
        return false;

    return EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType);
}

它的参数跟SignSignature函数差不多,所以这里的参数就不做过多的说明了。

例子如下:

cpp 复制代码
int main()
{
    LoadWallet();
    CTransaction coin = CreateTxPrev();  //构建引用的coin
    CKey newKey;//准备容器创建一个新账号,用来当收款人
    newKey.MakeNewKey();//创建公私钥
    CTransaction txNew = CreateTxNew(coin.GetHash(), newKey);
    //此时newTx还没构建完成,还需要添加签名,就是我们的签名验证例子
     
    if (SignSignature(coin, txNew, 0))      // nIn = 0
    {
        printf("SignSignature 成功!\n");
    }
    //txNew.vout[0].nValue = 100000000000;  //把这句注释取取消,改动交易,验证不会通过
    if (VerifySignature(coin, txNew, 0))//0 表示验证的是txNew里的vin[0].scriptSig.
    {
        printf("验证成功!\n");
    }
}

结果:

当我们改动txNew后,比如将里面的nValue修改,则会验证不通过。

为什么,因为txNew的哈希值跟签名不匹配了,我们来看一这个过程。

VerifySignature调用了如下代码:

cpp 复制代码
 return EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType);

而在Evalscript里,执行OP_CHECKSIG 操作码,调用了:

cpp 复制代码
  bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType);

在CheckSig中调用了:

cpp 复制代码
    if (key.Verify(SignatureHash(scriptCode, txTo, nIn, nHashType), vchSig))
        return true;

看到这个SignatureHash函数吗?它是根据txTo重新生成哈希值,你如果动了它,那哈希值肯定就跟之前的签名不匹配了。这个规则保证别人无法修改你的交易。只有知道你的私钥才行。

10.分层验证设计

注意,这里的签名验证,只是验证这笔交易,是否是对应的公钥进行签名,但至于这个引用的vout本身是否存在,脚本并不负责检查,这是比特币验证分为几个不同的层次导致。并不是漏洞。相关验证在其它的函数内实现,我们要知道签名,几乎就是对整个tx进行签名的,就是去除掉这个vin的其它签名(当前签名临时赋值公钥,签名正式生成后再替换),然后将这个tx序列化,进行哈希计算,获得哈希值,就对这个哈希值进行签名。

当然这个过程是可以控制的,你可以根据nHashType来选择对这个tx哪些部分进行签名,但一般我们不使用。所以你可以理解为是对整笔tx进行签名的,只是由于逻辑问题,需要对这个tx进行一些细微修改,要去掉其它vin的签名。然后对这笔tx进行哈希。所以你可以知道,脚本验证只负责哪些功能,他不负责检查你引用的vout是否在于区块链上,它只是一个底层的哈希值验证功能。

相关推荐
软件工程小施同学3 小时前
CCF A区块链论文分享-NDSS 2026(2)-CtPhishCapture:揭露针对加密货币钱包的基于凭证窃取的网络钓鱼诈骗(附pdf)
网络·pdf·区块链
Zhan8611247 小时前
数据接口的序列号机制与丢包检测:西班牙行情数据IBEX指数实时行情接入笔记
大数据·数据结构·笔记·区块链
CTA量化套保16 小时前
期货量化程序 time.sleep 卡死:天勤单线程与 deadline 替代
python·区块链
东方隐侠安全团队-千里20 小时前
币安Skills Hub:散户的“机构级超能力“来了
安全·ai·区块链·skills
终端域名20 小时前
AI与区块链融合:加密货币的下一前沿——技术架构、企业价值与未来趋势
人工智能·架构·区块链
Richown20 小时前
区块链治理:DAO与去中心化治理机制
区块链·react
终端域名20 小时前
密码学哈希函数:区块链 “不可篡改” 的核心数字指纹技术
区块链·密码学·哈希算法
2601_959480151 天前
Moneta Markets亿汇:“比特币回升提振风险情绪”
区块链
jrjrgood1 天前
黄金交易所有哪些正规的(全球版名单)
区块链