科普|从零开始学习比特币开发:生成地址
yuyutoo 2024-10-11 21:39 5 浏览 0 评论
生成地址
如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。
比特币核心提供了很多 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();
- 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,那么:
- 如果目标参数类型是 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)。
- 调用 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。
- 调用 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<CNoDestination, CKeyID, CScriptID, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessUnknown> CTxDestination,可能是以下几种类型之一:GetDestinationForKey 方法,使用 case 表达式来根据不同的地址类型,生成不同的目的 CTxDestination。
- 如果公钥不是压缩的,处理方法 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,这个返回值在下面两步中都会用到。
- 如果地址类型是 legacy,则直接返回公钥的 KeyID。内部把公钥的数据通过 SHA256 和 RIPEMD160 双重哈希之后,构造一个 CKeyID 对象。
- 如果地址类型是 p2sh-segwit,或 bech32,则处理如下:
- CNoDestination没有目的地设置
- CKeyIDP2PKH 目的
- CScriptIDP2SH 目的
- WitnessV0ScriptHashP2WSH 目的
- WitnessV0KeyHashP2WPKH 目的
- WitnessUnknown未知目的 P2W???
- 调用钱包对象的 SetAddressBook 方法,来保存公钥地址。
pwallet->SetAddressBook(dest, label, "receive");
- SetAddressBook 方法执行如下:
- 从 mapAddressBook 集合中,取得对应的目的数据。
std::map<CTxDestination, CAddressBookData>::iterator mi = mapAddressBook.find(address);
- 根据集合中是否有对应的数据设置变量是否为更新。
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 char> data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS);
- 对于主网络前缀是 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 char> vch(vchIn); uint256 hash = Hash(vch.begin(), vch.end()); vch.insert(vch.end(), (unsigned char*)&hash, (unsigned char*)&hash + 4); return EncodeBase58(vch);
- 在 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;
- 如果要数据的集合为为空,则返回假。
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
- 从密钥池中取出对应的公钥。
作者:区小白
来源:巴比特
[说明]以上内容采编自互联网,如内容侵犯您的版权,请联系邮箱:law@allwin.world,我们会在24小时内删除相关内容。
相关推荐
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
-
Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
-
概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...
- MySQL CREATE TABLE 简单设计模板交流
-
推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....
- mysql学习9:创建数据库(mysql5.5创建数据库)
-
前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
-
执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
-
在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
-
多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
-
全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...
- 【推荐】ProAc Response系列扬声器逐个看
-
有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
-
作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...
- 专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)
-
Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...
- 常用半导体中英对照表(建议收藏)(半导体英文术语)
-
作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...
- Fyne Audio F502SP 2.5音路低音反射式落地音箱评测
-
FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...
- 有哪些免费的内存数据库(In-Memory Database)
-
以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...
- RazorSQL Mac版(SQL数据库查询工具)
-
RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
- MySQL CREATE TABLE 简单设计模板交流
- mysql学习9:创建数据库(mysql5.5创建数据库)
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
- 【推荐】ProAc Response系列扬声器逐个看
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)