比特币源码分析之四:签名验证
在《比特币源码分析之三:交易脚本》文中最后以比特币系统中最简单的交易脚本为例子介绍了比特币的脚本指令系统,其中OP_CHECKSIG指令是该指令系统的核心指令,用于验证交易签名,本文重点介绍一下其原理。
ECDSA基础函数
椭圆曲线数字签名算法(ECDSA)是使用椭圆曲线密码(ECC)对数字签名算法(DSA)的模拟。
源码中有几个关键函数在这里简单介绍下方便下文的理解:
1、secp256k1_ecdsa_verify 用于使用公钥验证签名
函数原型:
int secp256k1_ecdsa_verify(const secp256k1_context* ctx, const secp256k1_ecdsa_signature *sig, const unsigned char *msg32, const secp256k1_pubkey *pubkey)
关键参数说明:
1)msg32 数据
2)sig 由msg32生成的签名(通过secp256k1_ecdsa_sign生成)
3)pubkey 公钥
返回值:
如果pubkey代表的公钥,对数据msg32的sig签名验证通过就返回true,否则返回false
2、secp256k1_ecdsa_sign 用于使用私钥生成签名
函数原型:
int secp256k1_ecdsa_sign(const secp256k1_context* ctx, secp256k1_ecdsa_signature *signature, const unsigned char *msg32, const unsigned char *seckey, secp256k1_nonce_function noncefp, const void* noncedata)
关键参数说明:
1)msg32 数据(与secp256k1_ecdsa_verify中的msg32对应)
2)signature 输出参数 生成的签名
3)seckey 公钥 (与secp256k1_ecdsa_verify中的pubkey组成非对称加密的公私钥对)
调用逻辑为,用户A使用私钥,通过secp256k1_ecdsa_sign函数对msg32做签名生成,生成signature,用户B使用公钥,通过secp256k1_ecdsa_verify对同样的数据msg32做sig验证,以此来证明用户B的公钥和用户A的私钥是一对。
签名验证的源码封装
在《交易脚本》文中提到了CKey和CPubKey两个类是比特币源码中代表私钥和公钥的两个类,而这两个类又提供了签名生成和验证的封装。
CKey::Sign用于生成签名
函数原型:
bool CKey::Sign(const uint256 &hash,std::vector<unsigned char>&vchSig,uint32_t test_case)
参数说明:
1)hash 代表交易的hash值,下文会详细介绍
2)vchSig 输出参数,代表生成的签名
功能介绍:
该函数是调用secp256k1_ecdsa_sign 使用ckey代表的私钥对hash(数据)运算生成签名
CPubKey::Verify 用于签名验证
函数原型:
bool CPubKey::Verify(const uint256 &hash, const std::vector<unsigned char>& vchSig)
参数说明:
Hash 代表交易hash
vchSig CKey::Sign函数生成的签名
返回值:
验证成功返回true否则返回false
OP_CHECKSIG逻辑
该指令的执行逻辑主要是从栈中取sig和pubkey调用TransactionSignatureChecker::CheckSig函数对签名进行验证
TransactionSignatureChecker::CheckSig 的执行逻辑如下
1、调用SignatureHash 对交易做hash运算
2、调用VerifySignature对第一步算出来的交易的hash做签名验证
VerifySignature 这个函数比较简单就是调用上文中的pubkey的Verify函数
SignatureHash
函数原型:
uint256 SignatureHash(const CScript& scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType, const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache)
参数说明:
1)scriptCode 输出脚本,这个对应的就是本次需要验证的交易的输出脚本(锁,ps:有时会是输出脚本的一段,这个逻辑暂时不考虑)
2)txTo 交易,也就是输入脚本(提供sig的脚本,钥匙)对应的交易(花钱的交易)
3)amount 花多少钱
4)sigversion,nHashType分别是交易结构组织方式和hash计算方式,这里先不讨论,以最简单的方式讲解,这里只需要知道这两个参数决定了hash的计算方式
函数功能:
该函数把花钱的交易的一些字段和输出脚本(出钱交易)一起做了一个hash处理,计算出了hash
可以简单理解为
Hash(tx pretx.outscript)
其中tx就是花钱的交易,也就是需要做脚本验证的交易,验证该脚本的输入是否合法(钥匙是否合法)
Pretx就是这个tx对应的上一笔tx,也就是待验证交易对应的出钱的交易
Outscript表示输出脚本,也就是pretx的锁
意义:
上面的解释有点绕,所以还是单独说一下这个设计的意义
还记得前面几篇关于交易的文章中提到的交易,其中交易如下图
本篇所谓的签名验证就是针对TxB做验证,验证TxB是否有花TxA提供的3个比特币的权利(是否有私钥)
上文中提到的Hash(tx pretx.outscript)公式中的tx就是TxB,pretx就是TxA 而outscript就是花3个比特币对应的输出脚本
具体的场景是这样的
1、用户B把自己的公钥做成一个地址(上一篇有介绍,这里简单理解为hash(pubkey))提供给用户A,让A给他打钱
2、用户A生成了一笔交易TxA,给这笔交易注入3个比特币,其中输出脚本中填入了用户B的公钥的地址(hash(pubkey)),并表示如果谁想花这3个比特币就必须提供两个数据
1)B的公钥
2)B的私钥生成的签名
3、用户B使用私钥生成了TxB,对TxB签名,并且提供了自己的公钥,把签名和公钥放入到输入脚本,满足了解开TxA的条件,也就是花了这笔钱
细心的读者可能还会有疑惑
1、输出脚本中为什么不是直接给一个pubkey,而是给了一个hash(pubkey)?
这个是为了保密考虑,直接把公钥提供出来不利于保密,而做一次hash,就可以在你不使用这笔钱的时候别人永远不知道你的公钥。
2、对整个tx做hash,但是hash后的签名又要写入到输入脚本内,这个是怎么做到的?
这个是上文为了简化模型做的错误描述,这里做hash是tx中除了输入脚本(sig和pubkey)之外的其他字段做hash,进而做签名。
3、为什么是对tx的除了输入脚本的其他所有字段做hash,而不是某个单一字段?单一字段做签名验证也能证明用户是持有私钥的。
举个例子
Tx中有一个字段是表述花费数量的,对应到上图就是TxB花费了2个比特币,如果我们在做hash的时候没有把2个比特币信息带进去,那么这个交易发布到网上的时候,矿主为了多赚手续费,可以把2个换成1个,那么手续费就从1个比特币变成了2个。所以对整个tx进行hash是为了保护一些字段不被串改。
Ps:其实也不是整个tx,有些字段为了灵活性是没有被hash的,尤其是一些特殊的场景,为了灵活性,故意不把字段做hash,但是这种情况的讲解需要对应场景,不利于理解,暂时不介绍。
下一篇会介绍另一个核心的概念:区块