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是否在于区块链上,它只是一个底层的哈希值验证功能。