内容简介:如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。比特币核心提供了很多 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 -
从密钥池中取出对应的公钥。
后记
由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First HTML5 Programming
Eric Freeman、Elisabeth Robson / O'Reilly Media / 2011-10-18 / USD 49.99
What can HTML5 do for you? If you're a web developer looking to use this new version of HTML, you might be wondering how much has really changed. Head First HTML5 Programming introduces the key featur......一起来看看 《Head First HTML5 Programming》 这本书的介绍吧!