主页 > imtoken海外版 > 科普|从零开始学比特币开发:生成地址

科普|从零开始学比特币开发:生成地址

imtoken海外版 2023-03-01 06:15:27

生成地址

如果有人要给你发比特币,或者你从别人那里买了几个比特币,你就得把地址给对方,这样对方才能把币发到你指定的地址。 那么,我们怎样才能有地址呢? 下面就来谈谈这个问题。

Bitcoin core提供了很多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 const wallet = GetWalletForJSONRPCRequest(request); CWallet* const pwallet = wallet.get();

GetWalletForJSONRPCRequest 方法的内部实现如下: 接下来,确保钱包可用。 如果钱包不可用,直接 NullUniValue 对象。

 if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { return NullUniValue; }

如果指定了帮助参数或者请求参数个数大于2,会显示钱包的帮助信息。 检查钱包是否设置了禁止私钥,即钱包是read-only 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]);

如果指定了地址类型btc地址怎么获取,则调用ParseOutputType方法检查地址类型,确保是legacy、p2sh-segwit、bech32中的一种,如果不指定,默认为p2sh-segwitbtc地址怎么获取,并将地址类型保存在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,这样就不会再推导子键了,所以其实这里基本没有执行这个方法. 并直接返回true。 调用钱包的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,那么:

CTxDestination witdest = WitnessV0KeyHash(key.GetID());

CScript witprog = GetScriptForDestination(witdest);

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,可以是以下类型之一: GetDestinationForKey方法,使用case表达式根据不同的地址类型生成不同的目标CTxDestination。

if (!key.IsCompressed()) return key.GetID();

CTxDestination witdest = WitnessV0KeyHash(key.GetID()); CScript witprog = GetScriptForDestination(witdest); if (type == OutputType::P2SH_SEGWIT) { return CScriptID(witprog); } else { return witdest; }

调用钱包对象的SetAddressBook方法保存公钥地址。

pwallet->SetAddressBook(dest, label, "receive");

SetAddressBook 方法执行如下:

std::map::iterator mi = mapAddressBook.find(address);

fUpdated = mi != mapAddressBook.end();

mapAddressBook[address].name = strName;

if (!strPurpose.empty()) mapAddressBook[address].purpose = strPurpose;

if (!strPurpose.empty() && !WalletBatch(*database).WritePurpose(EncodeDestination(address), strPurpose)) return false;

WalletBatch(*database).WriteName(EncodeDestination(address), strName);

调用 EncodeDestination 方法解码目标地址并返回结果。 EncodeDestination 方法也使用访问者模式 return boost::apply_visitor(DestinationEncoder(Params()), dest)。 DestinationEncoder类继承boost::static_visitor,实现visitor模式,通过重载()运算符定义不同类型的id。 与前面对应,该方法会处理CKeyID、CScriptID、WitnessV0KeyHash、WitnessV0ScriptHash、WitnessUnknown。 对于我们默认的情况,目标地址类型是CScriptID,我们看看如何处理这种情况,其他情况可以自行阅读。

std::vector data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS);

data.insert(data.end(), id.begin(), id.end());

return EncodeBase58Check(data);

std::vector vch(vchIn); uint256 hash = Hash(vch.begin(), vch.end()); vch.insert(vch.end(), (unsigned char*)&hash, (unsigned char*)&hash + 4); return EncodeBase58(vch);

GetKeyFromPool 从密钥池中获取公钥

此方法从密钥池中生成公钥。 第一个参数是对公钥的引用,第二个参数是internal,默认为false。

内部逻辑如下:

如果钱包禁止私钥,则返回 false。

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 方法。 该方法的执行流程如下:

nIndex = -1; keypool.vchPubKey = CPubKey();

if (!IsLocked()) TopUpKeyPool();

bool fReturningInternal = IsHDEnabled() && CanSupportFeature(FEATURE_HD_SPLIT) && fRequestedInternal;

bool use_split_keypool = set_pre_split_keypool.empty();

std::set& setKeyPool = use_split_keypool ? (fReturningInternal ? setInternalKeyPool : setExternalKeyPool) : set_pre_split_keypool;

if (setKeyPool.empty()) { return false; }

WalletBatch batch(*database);

auto it = setKeyPool.begin(); nIndex = *it; setKeyPool.erase(it);

if (!batch.ReadPool(nIndex, keypool)) { throw std::runtime_error(std::string(__func__) + ": read failed"); }

if (!HaveKey(keypool.vchPubKey.GetID())) { throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); }

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.erase(keypool.vchPubKey.GetID());

调用KeepKey从密钥池中获取对应的公钥。