内容简介:如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。比特币核心提供了很多 RPC 来供客户端调用,其中一个就是我们这里要讲的
生成地址
如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。
比特币核心提供了很多 RPC 来供客户端调用,其中一个就是我们这里要讲的 getnewaddress
生成一个新的地址,通过这个 RPC ,我们就可以生成一个新的地址,有了这个地址,别人就可以给我们转账了。
getnewaddress
RPC 可以接收两个参数,第一个地址的标签,第二个是地址的类型。如果没有提供标签,那么默认的标签就是空,地址的类型当前支持:legacy、p2sh-segwit、bech32,默认类型由 -addresstype
参数指定,当前为 p2sh-segwit。
如果我们想看下这个 RPC 的帮助文档,可以执行如下的命令:
./src/bitcoin-cli -regtest help getnewaddress
就会显示帮助信息
这个 RPC 对应的方法实现位于 src/wallet/rpcwallet.cpp
文件,方法名称就是 RPC 名称,下面我们来看这个方法。
生成地址流程
-
根据请求参数获得对应的钱包。
std::shared_ptr <cwallet> const wallet = GetWalletForJSONRPCRequest(request); CWallet* const pwallet = wallet.get(); </cwallet>
GetWalletForJSONRPCRequest
方法内部实现如下:-
调用
GetWalletNameFromJSONRPCRequest
方法,从请求对象中取得钱包的名字,如果用户指定了钱包名字,那么把钱包名字保存在参数wallet_name
上,并返回真,否则返回假。 -
如果可以获得用户指定的钱包名称,则调用
GetWallet
方法,从钱包集合vpwallets
中取得指定的钱包,然后返回钱包。 -
如果用户没有指定钱包或指定的钱包不存在,那么调用
GetWallets
方法,返回钱包集合vpwallets
。如果钱包集合中只有一个钱包,或者在用户指定了帮助的情况下,至少有一个以上的钱包,那么返回第一个钱包,即默认的钱包。默认钱包在系统启动时候创建的。
-
-
接下来,要确保钱包可用。如果钱包不可用,则直接
NullUniValue
对象。if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { return NullUniValue; }
-
如果指定了
help
参数或请求参数数量多于2个,则显示钱包的帮助信息。 -
检查钱包是否设置了禁止私钥,即钱包是只读的
watch-only/pubkeys
。如果是,则抛出异常。if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); }
-
如果指定了标签,则调用
LabelFromValue
方法,检查标签,确保其不是*
。如果是,则抛出异常。std::string label; if (!request.params[0].isNull()) label = LabelFromValue(request.params[0]);
-
如果指定了地址类型,则调用
ParseOutputType
方法,检查地址类型,确保其是legacy
、p2sh-segwit
、bech32
之一,如果不指定则默认是p2sh-segwit
,并把地址类型保存在output_type
变量中。OutputType output_type = pwallet->m_default_address_type; if (!request.params[1].isNull()) { if (!ParseOutputType(request.params[1].get_str(), output_type)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str())); } }
-
如果钱包没有被锁定,则调用
TopUpKeyPool
方法填充密钥池。if (!pwallet->IsLocked()) { pwallet->TopUpKeyPool(); }
TopUpKeyPool
填充密钥这个方法,我们前面已经讲过,这里只简单解释下,不做详细分析。因为在衍生子钥的过程中,setExternalKeyPool
、setInternalKeyPool
已经完全填充完了,所以导致missingExternal
、missingInternal
两个变量都为 0,从而不会重新再次衍生子密钥,所以实际上本方法在这里基本没有执行,而直接返回真。 -
调用钱包的
GetKeyFromPool
方法,从密钥池中生成一个公钥。如果不能生成,则抛出异常。CPubKey newKey; if (!pwallet->GetKeyFromPool(newKey)) { throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); }
GetKeyFromPool
方法,我们在下面详细讲解,此处略过。 -
调用钱包对象的
LearnRelatedScripts
方法,对公钥的脚本进行处理。方法内部执行如下:如果公钥是压缩的,并且地址类型是p2sh-segwit
,或者bech32
,那么:-
调用
WitnessV0KeyHash
方法,生成WitnessV0KeyHash
对象。CTxDestination witdest = WitnessV0KeyHash(key.GetID());
-
调用
GetScriptForDestination
方法,获取对应的脚本。CScript witprog = GetScriptForDestination(witdest);
GetScriptForDestination
方法内部调用boost::apply_visitor(CScriptVisitor(&script), dest)
,以访问者模式来根据不同的 id,获取其对应的脚本对象。CScriptVisitor
对象继承自boost::static_visitor
对象,实现了访问者模式,并通过重载()
操作符来定义不同类型的 id。-
如果目标参数类型是
CNoDestination
,则调用脚本对象的script
方法,清除脚本内容。 -
如果目标参数类型是
CKeyID
,则:首先调用脚本对象的script
方法,清除脚本内容;然后,初始化脚本*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG
。 -
如果目标参数类型是
CScriptID
,则:首先调用脚本对象的script
方法,清除脚本内容;然后,初始化脚本*script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL
。 -
如果目标参数类型是
WitnessV0KeyHash
,则:首先调用脚本对象的script
方法,清除脚本内容;然后,初始化脚本*script << OP_0 << ToByteVector(id)
。 -
如果目标参数类型是
WitnessV0ScriptHash
,则:首先调用脚本对象的script
方法,清除脚本内容;然后,初始化脚本*script << OP_0 << ToByteVector(id)
。 -
如果目标参数类型是
WitnessUnknown
,则:首先调用脚本对象的script
方法,清除脚本内容;然后,初始化脚本*script << CScript::EncodeOP_N(id.version) << std::vector(id.program, id.program + id.length)
。
-
-
调用
AddCScript
方法,保存脚本对象。AddCScript
方法,首先调用CCryptoKeyStore::AddCScript
方法,把脚本保存到 key store 的mapScripts
集合中;然后,调用数据库访问对象的WriteCScript
方法,以cscript
为键把脚本保存到数据库中。bool CWallet::AddCScript(const CScript& redeemScript) { if (!CCryptoKeyStore::AddCScript(redeemScript)) return false; return WalletBatch(*database).WriteCScript(Hash160(redeemScript), redeemScript); }
-
-
调用
GetDestinationForKey
方法,获取目的地CTxDestination
对象。CTxDestination
是一个具有特定目标的交易输出脚本模板。定义如下:typedef boost::variant
,可能是以下几种类型之一:CTxDestination -
CNoDestination没有目的地设置
-
CKeyIDP2PKH 目的
-
CScriptIDP2SH 目的
-
WitnessV0ScriptHashP2WSH 目的
-
WitnessV0KeyHashP2WPKH 目的
-
WitnessUnknown未知目的 P2W???
GetDestinationForKey
方法,使用case
表达式来根据不同的地址类型,生成不同的目的CTxDestination
。-
如果地址类型是
legacy
,则直接返回公钥的KeyID
。内部把公钥的数据通过 SHA256 和 RIPEMD160 双重哈希之后,构造一个CKeyID
对象。 -
如果地址类型是
p2sh-segwit
,或bech32
,则处理如下:-
如果公钥不是压缩的,处理方法
legacy
。if (!key.IsCompressed()) return key.GetID();
-
否则,生成
WitnessV0KeyHash
对象,然后调用GetScriptForDestination
方法,获取对应的脚本,最后根据不同的地址类型生成的目的。CTxDestination witdest = WitnessV0KeyHash(key.GetID()); CScript witprog = GetScriptForDestination(witdest); if (type == OutputType::P2SH_SEGWIT) { return CScriptID(witprog); } else { return witdest; }
对于默认的、不传地址类型的情况,就会返回
CScriptID
类型的CTxDestination
,这个返回值在下面两步中都会用到。
-
-
-
调用钱包对象的
SetAddressBook
方法,来保存公钥地址。pwallet->SetAddressBook(dest, label, "receive");
SetAddressBook
方法执行如下:-
从
mapAddressBook
集合中,取得对应的目的数据。std::map <ctxdestination nbsp="" caddressbookdata=""> ::iterator mi = mapAddressBook.find(address); </ctxdestination>
-
根据集合中是否有对应的数据设置变量是否为更新。
fUpdated = mi != mapAddressBook.end();
-
把标签保存为地址对应的
CAddressBookData
的name
属性。mapAddressBook[address].name = strName;
-
如果参数
strPurpose
不空,则更新地址对应的CAddressBookData
的purpose
属性。if (!strPurpose.empty()) mapAddressBook[address].purpose = strPurpose;
-
调用数据库访问对象的
WritePurpose
方法,保存参数strPurpose
到数据库中。if (!strPurpose.empty() && !WalletBatch(*database).WritePurpose(EncodeDestination(address), strPurpose)) return false;
-
调用数据库访问对象的
WritePurpose
方法,保存地址到数据库中。WalletBatch(*database).WriteName(EncodeDestination(address), strName);
strName
为用户提供的标签。EncodeDestination
方法,我们在下一步讲解。
-
-
调用
EncodeDestination
方法,解码目的地址,并返回其结果。EncodeDestination
方法同样采用了访问者模式return boost::apply_visitor(DestinationEncoder(Params()), dest)
。DestinationEncoder
类继承了boost::static_visitor
,实现了访问者模式,通过重载()
操作符来定义不同类型的 id。与前面相对应,这个方法会处理CKeyID
、CScriptID
、WitnessV0KeyHash
、WitnessV0ScriptHash
、WitnessUnknown
这几种不同情况。对于我们的默认情况来说,目的地址类型为CScriptID
,下面我们就看下这种情况的处理,其他情况可自行阅读。-
调用当前网络参数的
Base58Prefix
方法,返回脚本前缀。std::vector <unsigned nbsp="" char=""> data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS); </unsigned>
对于主网络前缀是 5,测试网络是 196,回归测试网络是 196。
-
把当前 20 个字节的数据加在前缀后面形成 21 个字节的字符串。
data.insert(data.end(), id.begin(), id.end());
-
调用
EncodeBase58Check
方法,编码成 Base58Check 格式,并返回其值。return EncodeBase58Check(data);
下面,我们来看下
EncodeBase58Check
这个方法的处理。它的内部执行流程如下:用 21 个字节的字符串生成一个向量,同时调用Hash
方法,生成一个 32 字节长的哈希字符串;然后把其最前面的 4个字节作为校验各加在 21 个字节的向量尾部,从而生成一个长度为 25个字节的字符串;最后,调用EncodeBase58
方法,进行 Base58 编码。std::vector <unsigned nbsp="" char=""> vch(vchIn); uint256 hash = Hash(vch.begin(), vch.end()); vch.insert(vch.end(), (unsigned char*)&hash, (unsigned char*)&hash + 4); return EncodeBase58(vch); </unsigned>
在
Hash
这个方法中,使用了双重 SHA256 哈希算法。EncodeBase58
这个方法,读者可以自行阅读,这里不再展开。
-
GetKeyFromPool 从密钥池中获取公钥
本方法从密钥池中生成一个公钥。第一个参数为公钥的引用,第二个参数
internal
,默认为假。
内部逻辑如下:
-
如果钱包禁止私钥,则返回假。
if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { return false; }
-
调用
ReserveKeyFromKeyPool
方法,从密钥池中取出一个密钥并获取其公钥。如果不成功,则生成数据库访问对象,然后调用GenerateNewKey
方法,生成一个公钥。if (!ReserveKeyFromKeyPool(nIndex, keypool, internal)) { if (IsLocked()) return false; WalletBatch batch(*database); result = GenerateNewKey(batch, internal); return true; }
GenerateNewKey
这个方法,在创建钱包过程中,我们已经重点分析,这里不浪费口舌,我们重点看下ReserveKeyFromKeyPool
方法。这个方法的执行流程如下:-
生成一个公钥,并设置为密钥池的
vchPubKey
属性。nIndex = -1; keypool.vchPubKey = CPubKey();
-
如果钱包没有被锁,则填充密钥池。
if (!IsLocked()) TopUpKeyPool();
TopUpKeyPool
这个方法,我们也讲过,这里直接略过。 -
如果钱包启用了 HD,并且可以支持 HD 分割,并且参数
fRequestedInternal
为真,则设置变量fReturningInternal
为真。在调用本方法时,这个参数没有指定,而默认为假,所以变量fRequestedInternal
设置假。bool fReturningInternal = IsHDEnabled() && CanSupportFeature(FEATURE_HD_SPLIT) && fRequestedInternal;
-
根据集合
set_pre_split_keypool
是否为空,设置变量use_split_keypool
的值。因为这里use_split_keypool
集合为空,所以变量use_split_keypool
为真。bool use_split_keypool = set_pre_split_keypool.empty();
-
根据变量
use_split_keypool
、fReturningInternal
确定从哪个集合中获取密钥池对象。根据上面分析,我们最终会从setExternalKeyPool
集合中取数据。std::set <int64_t> & setKeyPool = use_split_keypool ? (fReturningInternal ? setInternalKeyPool : setExternalKeyPool) : set_pre_split_keypool; </int64_t>
-
如果要数据的集合为为空,则返回假。
if (setKeyPool.empty()) { return false; }
-
生成数据库访问对象。
WalletBatch batch(*database);
-
从
setKeyPool
取得其第一个元素,并从集合中删除它。auto it = setKeyPool.begin(); nIndex = *it; setKeyPool.erase(it);
-
从数据库取得索引对应的密钥池。如果失败,则抛出异常。
if (!batch.ReadPool(nIndex, keypool)) { throw std::runtime_error(std::string(__func__) + ": read failed"); }
-
从密钥池中取得公钥对应的 ID,并且检测其是否在
mapKeys
、或mapCryptedKeys
集合之一,如果不在,则抛出异常。我们在创建钱包过程时候讲过,生成的私钥根据是否加密会保存在这两个集合之一。if (!HaveKey(keypool.vchPubKey.GetID())) { throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); }
-
如果变量
use_split_keypool
为真,并且密钥池的fInternal
属性不等于变量fReturningInternal
,那么抛出异常。if (use_split_keypool && keypool.fInternal != fReturningInternal) { throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); }
-
如果密钥池中保存的公钥是无效的,那么抛出异常。
if (!keypool.vchPubKey.IsValid()) { throw std::runtime_error(std::string(__func__) + ": keypool entry invalid"); }
-
从
m_pool_key_to_index
集合中消除对应的索引。m_pool_key_to_index.erase(keypool.vchPubKey.GetID());
-
返回真。
-
-
调用
KeepKey
-
从密钥池中取出对应的公钥。
后记
由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- bitcoin地址是如何生成的
- Java离线生成比特币地址
- Flutter / Dart生成以太坊地址
- Laravel 生成 Gravatar 头像地址的优雅姿势
- 使用PHP从扩展公钥生成比特币钱包地址
- 学习 btc 钱包私钥、公钥和地址的生成过程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Effective Engineer
Edmond Lau / The Effective Bookshelf, Palo Alto, CA. / 2015-3-19 / USD 39.00
Introducing The Effective Engineer — the only book designed specifically for today's software engineers, based on extensive interviews with engineering leaders at top tech companies, and packed with h......一起来看看 《The Effective Engineer》 这本书的介绍吧!