BTC 区块链原理

区块链(block chain):一种分布式数据库技术。以区块为单位存储数据,区块们以哈希链的形式串联,因此防伪。

  • 区块链起源于比特币(BTC),用于记录交易信息,每笔交易相当于 SQL 数据库的一个事务。
  • 本文主要分析比特币协议中的区块链原理(协议更新之后,一些细节可能变化)。
  • 参考资料:

区块结构

  • 如下图,一条区块链由多个区块串联组成,后一个区块会记录前一个区块的哈希值,构成哈希链。
    请添加图片描述

    • 哈希运算时采用 SHA256 算法。
    • 存储字节流时采用小端序。
    • 每个区块采用递增的序号作为标识符,又称为区块高度(Height)。
    • 每个区块最多允许包含 1MB 的数据,超过则视作无效区块。
  • 一个区块在结构上分为两部分:

    • header :用于存储该区块的元数据。
    • body :用于存储交易信息。
  • 区块 header 的长度固定为 80 bytes ,按顺序记录以下信息:

    • nVersion
      • :区块链协议的版本。
      • 4 bytes ,int32_t 。
    • previous block header hash
      • :上一个区块的 header 的哈希值。用于确保它们不会被篡改。
      • 32 bytes ,char[32] 。
    • merkle root hash
      • :当前区块 body 中所有交易信息的 Merkle Tree 的根哈希。用于确保它们不会被篡改。
      • 32 bytes ,char[32] 。
    • timestamp
      • :矿工打包当前区块时的 Unix time 时间戳。必须大于前 11 个区块的平均时间戳、不大于当前实际时间 +2 小时。
      • 4 bytes ,uint32_t 。
    • nBits(target threshold)
      • :nonce 的目标阈值。
      • 4 bytes ,uint32_t 。
    • nonce
      • :一个随机数。
      • 4 bytes ,uint32_t 。

挖矿难度

  • 矿工打包新区块时,需要尝试指定 nonce 值,比如穷举。如果使得当前 header 的哈希值小于等于 target threshold ,则有权打包该区块,被其他矿工承认。
    • SHA256 哈希值的长度为 32 bytes ,而 target threshold 以有损压缩形式存储为 nBits ,长度为 4 bytes 。
    • 例:根据 nBits 计算出 target threshold
      >>> nBits  = int('0x170cfecf', 16)                              # 假设 nBits 的取值
      >>> target = 256**(int('0x17', 16) - 3) * int('0x0cfecf', 16)   # 将第一个字节作为 256 的幂,再乘以后三个字节
      >>> '0x' + "{:064x}".format(target)
      '0x0000000000000000000cfecf0000000000000000000000000000000000000000'
      
      • 可见 nBits 第一个字节的值越大,会使 taget 越大,开头连续的 0 越少,因此挖矿难度越小。
  • difficulty 表示挖矿难度。
    • 计算公式如下:
      diffculty = difficulty_1_target / target
      
      • 可见 difficulty 与 target 成反比,取值越小则挖矿难度越小,最小为 1 。
    • difficulty_1_target 表示区块链的初始难度,是一个常数:
      >>> difficulty_1_target = 2**(256-32)-1
      >>> '0x' + "{:064x}".format(difficulty_1_target)
      '0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
      >>> difficulty_1_target / target
      21659675333681.926
      
    • 当 difficulty 为 1 时,target 达到允许的最大值 ‘0x1d00FFFF’ 。因为有损压缩丢失了右侧的所有 ‘0xF’ ,所以有的程序会将 difficulty_1_target 计算成偏小的值:
      >>> _target = 256**(int('0x1d', 16) - 3) * int('0x00FFFF', 16)
      >>> '0x' + "{:064x}".format(_target)
      '0x00000000ffff0000000000000000000000000000000000000000000000000000'
      >>> _target / target
      21659344833264.848
      
      • 丢失右侧 ‘0xF’ 的 difficulty_1_target 常用于快速估算难度,称为 bdiff(Bitcoin difficulty)。
      • 标准的 difficulty_1_target 常用于正式挖矿,称为 pdiff(Pool difficulty)。
    • 例:根据 nBits 估算出 difficulty
      >>> nBits      = int('0x170cfecf', 16)
      >>> difficulty = 256**(29 - (nBits >> 24))*(65535.0 / ((float)(nBits & 0xFFFFFF)))
      >>> difficulty
      21659344833264.848
      
    • BTC 预计每隔 10 分钟生成一个新区块。为了维持这一速率,会每隔 2016 个区块调整一次挖矿难度,重新计算 nBits :
      expected_time  = 2016*10    # 理论上最近 2016 个区块应该消耗 2016*10 分钟即 2 周
      actual_time    = ...        # 实际上最近 2016 个区块消耗的时长
      new_difficulty = old_difficulty * ( actual_time / expected_time )
      new_nBits      = ...        # 根据 new_difficulty 算出 new_nBits
      
  • 理论上,SHA256 哈希值有 2^256 种可能性,而 nonce 只有 2^32 种可能性,因此可能穷举 nonce 的所有值之后依然不满足 target threshold 。
    • 例如 2020 年发布的蚂蚁矿机 S19 ,额定算力为 95 THash/s ,遍历 nonce 所有值的耗时不超过 1 秒:
      >>> (2**32) / (95*10**9)
      0.045
      
    • 当 nonce 被穷举完时,矿工通常会在 coinbase 交易中使用一个 4 bytes 的随机数,称为 extraNonce ,从而增加到 2^64 种可能性。
    • 例如 2020 年的比特币全网算力达到了 150 EHash/s = 150 * 10^3 PHash/s = 150 * 10^6 THash/s ,遍历 nonce + extraNonce 消耗的秒数为:
      >>> round((2**64) / (150*10**15))
      123
      

      因此挖矿难度自动上调得很大,从而限制产生新区块的耗时依然为 10 分钟。矿工还需要用其它变量来增加可能性。

账户

  • BTC 协议中,用户可以根据 ECDSA 算法随机生成一对私钥、公钥,代表一个 BTC 账户,相当于一个银行账户的密码、账户名。

    • 示例:
      0xL3HpQs5M7tZLN1m6zmJ9YSPuFzA4gJDJy8Ru2hd5nAwcZC4tPm1T  # 私钥
      0x1N8nCD314Z87BgYsK1y3vwRpAk8S1qbRcg                    # 公钥
      
      • 前缀 0x 表示十六进制。
      • 私钥需要保密,而公钥公开给其他人。
    • BTC 采用椭圆曲线数字签名算法(ECDSA)对交易数据进行数字签名,其中采用的椭圆曲线为 secp256k1 。
      • 用户可以编写一个消息,将消息的哈希值用私钥加密之后公布,称为数字签名。其他人可使用对应的公钥解读数字签名,从而验证该消息的内容没有被篡改,并且是由该公钥对应的私钥签署的。
    • 虚荣地址(Vanity Addresses):虽然 BTC 私钥、公钥是随机的,但用户可以尝试生成大量私钥、公钥,直到公钥中包含有意义的单词,比如 ILoveU 。
    • bitaddress :一个网站,用于随机生成 BTC 账户,支持离线使用。
  • 生成一对私钥、公钥的步骤:

    1. 用户生成一个很大的随机整数,作为私钥(private key)。

      • 私钥的长度为 256 bits = 32 bytes 。通常经过 Base58Check 编码,表示成 52 位长度的十六进制数。
      • 用户选择私钥时应该尽量随机,避免与其他人相同。
    2. 根据 ECDSA 算法,计算出私钥在椭圆曲线上对应的一个点,其坐标 x、y 作为公钥。

      • 通过私钥可以计算出对应的公钥,但不能通过公钥反推出私钥。而且值域庞大,使得穷举几乎不可能。
      • 未压缩公钥(uncompressed):拼接坐标 x、y 的值,并加上前缀 0x04 用于区分,总长度为 1+32+32 = 65 bytes 。
      • 压缩公钥(compressed):只采用坐标 x 的值,并加上前缀 0x02 或 0x03 表示坐标 y 为偶数或奇数,总长度为 33 bytes 。
        • 椭圆曲线关于 x 轴对称,一个坐标 x 对应两个 y 点。且在 secp256k1 曲线中,这两个 y 点分别为偶数、奇数,因此需要通过前缀区分。
        • 使用压缩公钥时,需要给私钥加上后缀 0x01 用于区分,其长度增加 1 字节。
        • 压缩格式的公钥、私钥更方便记录,是用户通常看到的格式,也是一般钱包采用的导入格式(Wallet Import Format,WIF)。
        • 钱包软件在实际使用公钥时,会从压缩格式转换成非压缩格式。
    3. 计算压缩公钥的 SHA256 哈希值,再计算其结果的 RIPEMD-160 哈希值。

    4. 将公钥哈希值(public key hash)经过 Base58Check 编码,表示成 34 位长度的十六进制数,称为账户地址(address)。

  • Base58Check 编码是为了将数据表示成更短的值,并加上校验码,并不会加密原数据。过程如下:

    1. 输入原数据 payload 。
    2. 在 payload 之前加上 1 字节的地址版本号 version 。
      • P2PKH 地址的 version 为 0x00 ,因此 base58 编码之后,开头为 1 。
      • P2SH 地址的 version 为 0x05 ,因此编码之后,开头为 3 。
      • BTC 私钥的 version 为 0x80 ,因此编码之后,未压缩格式的开头为 5 ,压缩格式的开头为 K 或 L 。
    3. 对 version-payload 计算两次哈希值,取开头的 4 字节作为校验码 checksum 。
      • 如果网络传输之后,再次计算两次哈希值,发现开头与校验码不一致,则说明编码值出错。
    4. 在 payload 末尾加上校验码 checksum ,将 version-payload-checksum 进行 base58 编码。
      • 与 base64 相比,base58 的特点:
        • 移除了 +/ 两个特殊字符,只允许使用大小写字母、数字。
        • 移除了 0OIi 四个容易混淆的字母。

交易

  • block body 中可以包含 n≥1 笔交易(transaction),最大数量取决于区块的容量限制。

    • 包含的第一笔交易必须是 coinbase 交易。
      • coinbase 交易不存在输入,凭空产生一定量可以输出的 BTC ,作为打包该区块的矿工的奖励。
    • 其后可以包含 m≥0 笔普通交易。
      • 当交易被保存到区块中之后,其内容就不能被改变。
      • 通常采用交易的哈希值作为其标识符,称为 txid 。
      • BTC 交易时,最小的单位为聪(satoshis)。1 BTC = 10^8 聪。
  • 一个交易主要包含以下内容:

    • version :交易格式的版本号,占 4 bytes 。
    • in-counter :表示输入项的数量。
    • inputs :包含 n≥1 个输入项。
      • 每个输入项的主要内容:
        • index :输入项在 inputs 中的序号,从 0 开始递增。
        • previous txid :指向输入的 BTC 来自的之前交易。这会将该交易的 UTXO 全部输入当前交易。
        • sigScript
    • out-counter :表示输出项的数量。
    • outputs :包含 n≥1 个输出项。
      • 每个输出项的主要内容:
        • index
        • value :输出的 BTC 数量。
        • pkScript
      • 一个交易的总输入减去总输出的差值,会被矿工作为手续费,即 fee = sum(inputs) – sum(outputs)
    • locktime :表示允许将该交易打包到区块中的最早时间,占 4 bytes 。
      • 如果取值小于 5 亿,则视作区块高度。
      • 如果取值大于 5 亿,则视作 Unix 时间戳。
      • 交易通常将 locktime 设置为 0x00000000 ,表示不限制。
  • 如果一个账户收到了一些 BTC ,则可以发起一笔交易,输入一些 BTC ,然后输出给其它账户。

    • 所有账户收到的 BTC ,都来自历史交易的输出,且源头都是 coinbase 交易。
    • 如果一个交易输出的 BTC ,尚未被目标账户花费,则称为未花费交易输出(Unspent TX Output,UTXO)。
      • 一个账户拥有的所有 UTXO ,称为该账户的余额。
      • 一个交易的 UTXO 必须被一次性全部花费。例如 UTXO 为 5 BTC 时,用户可以新建一个交易,转账 1 BTC 给别人,然后将剩下的 BTC 转账给自己。
    • 统计 BTC 区块链上发生的所有交易的输入、输出,就可以知道所有账户的余额。
      • BTC 并没有提供直接查询账户余额的数据库,但一些区块链浏览器提供了这种查询功能。
  • 发起一个交易的过程如下:

    1. 用户编写一个交易。
    2. 用户生成交易的 sigScript ,存放在交易的 inputs 中。
    3. 用户将交易广播到 BTC 网络上,等待矿工将它打包入新区块。

sigScript

:签名脚本(Signature Script,sigScript)。

  • sigScript 包含以下内容:
    • pubKey :账户公钥
    • sig :使用账户私钥,生成交易数据的数字签名。
  • 交易延展性(Transaction Malleability)攻击
    • :一种攻击方式,指改变未打包交易中的签名,使得交易的哈希值改变,导致引用该交易 txid 的其它交易失效。
      • 由于椭圆曲线的对称性,可以计算出两个有效的签名,还可以根据任意一个签名推算出另一个签名。
    • 常见的解决办法:
      • 等待一个交易被打包,再引用它的 txid 。但这就不能实现闪电交易。
      • 规定只采用取值较小的那个签名。
      • 使用 SegWit 。

pkScript

:公钥脚本(Pubkey Script,pkScript),采用一种基于堆栈的脚本语言,包含一些指令。

  • pkScript 可以声明一些条件,当 sigScript 满足条件时才能花费 UTXO 。

    • 比如 P2PKH 通常的条件是:输出 n 个 BTC ,并指定一个公钥。只有拥有该公钥对应私钥的人,才有权花费该 UTXO 。
  • pkScript 又称为锁定脚本(locking script),因为它输出的 BTC 一直不可用,直到有人提供满足条件的 sigScript 。

  • pkScript 的几种类型:

    • Pay To Pubkey(P2PK)
      • :将 BTC 转账给公钥。
    • Pay To Public Key Hash(P2PKH)
      • :将 BTC 转账给公钥哈希。
      • P2PK 常用于 BTC 早期的交易,后来被 P2PKH 取代。主要原因:
        • P2PKH 使用公钥哈希,更短。
        • P2PKH 使用公钥哈希,隐藏了公钥,提供了额外的安全性。直到用户花费该账户时,才会提供公钥。
    • Pay To Script Hash(P2SH)
      • :将 BTC 转账给脚本哈希。
        • 如果其他人提供的 sigScript 中,包含与 P2SH 哈希一致的脚本,称为赎回脚本(redeem script),则有权花费该 UTXO 。
      • 通过赎回脚本可实现复杂的功能,例如多重签名、SegWit 兼容地址。
    • Pay to Witness Pubkey Hash(P2WPKH)
      • :与 P2PKH 类似,但启用了 SegWit 。
    • Pay to Witness Script Hash(P2WSH)
    • Null Data(空数据)
      • :用于在 pkScript 中添加任意字节的空数据。

隔离见证

:隔离见证,一种 BTC 的扩容方案,属于软分叉升级。

  • 原理:给区块附加一个称为 witness 的结构,将交易中的见证数据(主要包含 sigScript )移出,存放在 witness 区域。

    • 这会减小交易的一大半体积,可以在区块中打包更多交易。
    • witness 区域的 tree 哈希记录在 coinbase 交易中,从而嵌入 Merkle Tree 。
    • 此时的区块称为 SegWit 格式。基础容量(称为 size )依然限制为 1MB ,附加 witness 数据时的总体积(称为 weight )限制为 4MB 。
    • 此时交易的哈希值称为 wtxid 。改变 sigScript 时,不会影响 wtxid ,避免了 Transaction Malleability 攻击。
  • 使用 SegWit 时,账户地址有两种格式:

    • 原生地址(Native):采用 Bech32 编码格式,只允许使用小写字母、数字,长度为 42 位,开头为 bc1 。
    • 兼容地址(Nested):采用 P2SH 地址,开头为 3 。
      • 原生地址地址的效率比兼容地址更高。
      • 传统地址(Legacy)不支持与 SegWit 原生地址转账。
      • SegWit 兼容地址支持与传统地址、SegWit 原生地址转账。
  • SegWit 软分叉还涉及到 BTC 社区的治理问题,相关历史:

    • 2015 年,Bitcoin Core 开发人员提出了 BIP141 ,提议 SegWit ,是此时 BTC 变动最大的一次软分叉升级。
    • 2016 年,Bitcoin Core 按 BIP9 方式开始 SegWit 软分叉,但支持 SegWit 的矿工远未达到 95% 的激活阈值,主要原因:
      • SegWit 尚未推广,早期升级的收益较小。
      • 旧区块可通过 AsicBoost 算法提高挖矿的哈希算力。
      • SegWit 增加了区块的复杂性,需要修改很多客户端代码,不如硬分叉简单。
    • 2017 年 2 月,社区的开发人员提出了 BIP148 ,提议用户激活软分叉(User Activated Soft Fork,UASF),逼迫矿工激活 SegWit 。
      • BIP148 大意为:从 8 月开始,采用 BIP148 的节点会拒绝不支持 SegWit 的新区块,导致发生硬分叉。
      • BIP9 让矿工投票决定软分叉升级,而 UASF 使得用户也拥有了控制权。但这更像一个抗议行动,不支持的矿工只是少了打包这部分用户交易的手续费收益。
    • 2017 年 4 月,Charlie Lee 与矿工协商之后,成功在 LTC 上激活 SegWit 。
    • 2017 年 5 月,一些公司和矿池签署了纽约共识(New York Agreement,NYA),表示作为激活 SegWit 的交换,要求也激活 SegWit2x 。
      • 原理:与 SegWit 不同,直接将区块的基础容量限制从 1MB 增加到 2MB ,属于硬分叉升级
      • SegWit2x 与 BCH 的区块扩容类似,受到 Bitcoin Core 开发人员的反对,最终在 11 月放弃了该提议。
    • 2017 年 5 月,一个开发人员提出了 BIP91 ,使得 BIP148 与 SegWit2x 两派可以兼容,避免发生硬分叉。
      • BIP91 的内容与 BIP148 相似,但没有限制时间。
      • BIP91 部署之后,激活它的阈值为 80% 。当 BIP141 被激活或失败时,BIP91 就会停止。
    • 2017 年 7 月,市场担心 BIP148 硬分叉的风险,BTC 价格从 $2700 跌到 $2000 ,促使矿工们支持 BIP91 并激活它。
    • 2017 年 8 月,SegWit2x 终于激活。
      • 此时,大部分矿工、钱包软件依然没有升级 SegWit 。但是采用 SegWit 原生地址的用户越来越多,为了服务这部分用户,矿工、钱包软件也会逐渐升级 SegWit 。

合约交易

多重签名

:多重签名(multi-signature,multisig),一种基于 P2SH 的交易方式。

  • 原理:将 BTC 转账到一个 P2SH 地址,通过 pkScript 指定 n 个公钥,要求 sigscript 至少用 m 个对应的私钥签名才有效。
    • 其中 1≤m≤n≤15 ,又称为 m-of-n 交易。
  • 1of2 是允许两个账户中的任意一个都有权花费 UTXO 。
  • 2of3 适合三方交易。例如 A 想花费 BTC 从 B 处购买商品,先将 BTC 转账到 2of3 多签地址。
    • 如果仲裁方判断交易成功,则与 B 一起签名,将 BTC 转账给 B 。
    • 如果仲裁方判断交易失败,则与 A 一起签名,将 BTC 转账给 A 。
    • 没有 A 或 B 的同意,仲裁方不能私自转走 BTC 。

闪电交易

:闪电交易(Lightning Network,LN),一种 layer2 技术,基于链下交易(Off-Chain)。

  • 官方文档

  • 原理:在区块链下进行任意数量的交易,然后将这些交易的结果保存到区块链。主要步骤如下:

    1. 开启通道:两个账号将一笔 BTC 转账到一个多签地址,暂时锁定,称为开启一个闪电交易通道(channel)。
    2. 使用通道:双方建立对等连接(Peer connection),私下进行一些交易。
    3. 关闭通道:双方根据交易结果,将多签地址的 BTC 分配给双方。
  • 在通道中,双方私下进行的交易只需要互相知道,不必公布到区块链上,称为承诺交易(commitment transaction)。

    • 每个承诺交易,表示此时的通道状态,即双方应该分别拥有通道的多少 UTXO 。
    • 每创建一个新的承诺交易,就撤销旧的承诺交易。
      • 为此双方要交换一个承诺撤销密钥,证明它已撤销。
      • 如果一方将已撤销的承诺交易广播到链上,企图双花攻击。则另一方可以根据撤销密钥,广播一个更新的惩罚交易(Penalty transaction),将通道的全部资产转给自己。
    • 每个承诺交易设置了 locktime ,如果某方离线,则另一方等待 locktime 时间之后就可以将承诺交易公布到链上,从而关闭通道。
      • 每个承诺交易的 locktime 依次递减,因此越新的承诺交易能越早公布到链上。
      • 准备关闭通道时,双方签署最后一笔承诺交易,立即公布到链上。
  • 闪电交易通道只能连通两个账户,但大量相互开启通道的账户可以组成闪电交易网络。

    • 假设 A 与 B 之间存在通道,B 与 C 之间存在通道,则 A 可以通过 B 间接转账给 C 。此时 B 称为路由节点,像计算机网络的路由转发。
    • 哈希时间锁定合约(Hash Time Locked Contracts,HTLC):一种智能合约,用于保证路由节点的可信任。工作流程如下:
      1. A 创建一个密钥 secret ,私下发送给 C 。
      2. A 在 A、B 之间的通道,使用 secret 的哈希值签署一个 HTLC 合约,将 BTC 转入合约。合约大意为:如果 B 能在 locktime 时间内发现 secret ,则有权获得合约的 UTXO ,否则资金将返回给 A 。
      3. B 在 B、C 之间的通道,也创建一个使用同样 secret 哈希值的 HTLC 合约,请求 C 提供 secret 。但是合约锁定的 UTXO 微少一点,因为 B 收取了手续费。locktime 也更短一点。
      4. C 提供 secret ,完成 B 的 HTLC 合约,得到 UTXO 。然后 B 再提供 secret ,完成 A 的 HTLC 合约。
        • 即使 B 下线,C 依然可以完成 B 的 HTLC 合约,只有 B 会受到损失。
    • 一些大型的路由节点可连接大量用户,像中心化网络。
    • 一些路由节点会担任瞭望塔(watchtower),监控网络,广播惩罚交易。
  • 优点:

    • TPS 很高
    • 手续费很低:支付给路由节点的费用很低,适合以 satoshis 为单位的小额交易。
    • 耗时短:每个交易不超过一分钟,不需要等待区块链打包、确认。
    • 隐私:只有开启通道、关闭通道的两次交易需要公布到链上,期间的交易不会上链,路由节点也不会知道整个交易的源头、终点。
    • 耗费区块空间小:通道中的交易不必保存到区块链。
  • 相关历史:

    • 2017 年 5 月,LTC 区块链进行了第一次闪电交易。
    • 2019 年 1 月,twitter 用户发起了一场称为闪电火炬(Lightning Torch)的行动,用于推广闪电交易。
      • 规则:通过闪电交易发送一笔小额 BTC ,收到转账的账户添加 10w satoshis 之后再发送给下一个账户,以此类推。
      • 这笔 BTC 传递了将近 300 次,最终被捐赠。

CoinJoin

:一种混币方案。

  • 原理:n 个账户共同发起一笔 BTC 交易,分别输入 C 数量的 BTC ,然后分别输出 C 数量到 n 个账户。

    • 如果输出账户与输入账户都不同,则难以确定它们之间的对应关系,任一输出账户受某个输入账户所有人控制的概率是 1/n 。因此即使输入账户暴露了现实身份,输出账户也重新得到了匿名性。
    • n 的数量越大,混淆度越高。
  • BTC 账户具有匿名性,但账户的交易过程、余额都是公开的,难以保护隐私。

    • 例如用户 A 使用一个 BTC 账户在网上购物,转账记录都是公开的,其他人可以知道他购买的所有商品,推测他的消费习惯、个人喜好。
      • 如果购物时的 IP 地址、物流信息被泄露,账户就失去了匿名性。即使 A 将 BTC 转入其它账户,其他人也知道新账户的地址,推测新账户很可能属于他。
  • 混币(Coin Mixing)

    • :指混淆多个账户的交易过程,使得难以确定每个账户的收入来自哪个账户、支出去向哪个账户,只能知道每个账户收入、支出的数量以及余额。
    • 常见的混币方案:
      • 通过第三方平台转账:比如将币转入中心化交易所,再由从交易所转出到其它账户地址。但这样需要担心交易所的提现风险,而且交易所也能查出交易过程、记录 KYC 信息。
      • CoinJoin
    • 闪电交易也加强了隐私,但并不属于混币,而是避免交易过程上链。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>