toBeTheLight.github.io 荒原

从区块链到 NFT 到元宇宙

2022-03-12
toBeTheLight

区块链

以比特币白皮书来说,区块链的本质是一个 P2P(点对点)的分布式账本,节点发出交易时需要向周围节点广播,全节点(还有轻节点等,轻节点一般只保存和自己有关的数据)将收到的交易信息写入区块体中,在共识机制(PoW 即挖矿,还有其他如 PoS、DPoS 机制)作用下获取到记账的权限(即认证某些交易数据,向区块链中写入区块的权限),将新区块广播至周围节点,其它节点校验数据有效性和记账权限后根据区块数据同步本地状态,从而达成整个区块网络的状态一致,同时由于新区块头中有前一区块的关联信息,从而使区块之间达成连接,形成区块链。

简单来说,就是网络中所有结点做完全相同的操作在本地维护一个状态一致性的分布式系统。

区块链结构

  • 区块链结构:区块 + 哈希链接
    • 链:新的区块通过区块间哈希(哈希算法是一种固定输入产生固定输出的算法,可以理解为对输入产生了一个身份ID)关联,区块间通过这种关联成链
    • 区块:
      • 区块体:
        • 区块体的结构往往是一颗树
        • 树的叶子结点存储实际数据,即账本信息(如交易信息)等
        • 两两向上逐层求得哈希值形成哈希树(即 Merkle Tree 或使用其变种结构)
      • 区块头:负责基本功能,一般包含以下部分
        • 对前一个区块的哈希求值(仅对前一区块的区块头的内容进行哈希运算)
        • 哈希树的根哈希
        • 时间戳

Merkle Tree

  • 防篡改:
    • 单个块:当单个块中的交易信息被某个结点修改后,势必会逐层向上影响至存储在区块头中的哈希树的根哈希,那么就需要修改根哈希才能达成单个区块数据篡改有效性
    • 链:如果某个区块中的根哈希被修改,那么对这个区块的区块头进行哈希运算必然会产生新值,和它的下一个区块中的对它的哈希引用的数值对不上,则必然会影响到后续整个链上的所有区块,所以需要修改后续的所有区块才能达成篡改的有效性
    • 这种对整个区块链篡改的可行性需要破坏已形成的共识,需要有压制性的优势,理论上在参与者众多的系统中很难达成,从而保证了区块链的防篡改特征

比特币

状态

比特币的节点在本地维护了一个账本叫做 UTXO(Unspent Transaction Output)的状态,即未使用的交易输出,可以简单的理解为某笔未使用的转账输入,用于进行后续转账的转出使用。

运行机制

比特币区块的区块体中以哈希树的结构存储了新发生的每笔交易。

单笔交易的一种协议信息的示例如下:

In: 
// 表示这次交易使用的资金的来源
    Previous tx: f5d8...430901c91
    // 资金来源所在交易的哈希
    Index: 0
    // 资金来源交易哈希中的哪个转出
    scriptSig: 3045...798a4 618c...41501
    // 对声明的资金来源的解锁脚本,含有签名和公钥
    // 可以简单理解为银行卡号和密码

Out: // 表明这次交易的输出
    Value: 5000000000
    // 金额
    scriptPubKey: OP_DUP OP_HASH160 4043...549d OP_EQUALVERIFY OP_CHECKSIG
    // 锁定脚本,含有对公钥的哈希和一些运算符,使用这笔转出的交易需要在它的 scriptSig 提供可以通过脚本的信息才能解锁
    // 可以理解为银行卡号的哈希和一些操作指令
  • 非对称加密概念科普:
    • 公钥、私钥是是成对生成的
    • 公钥公开给系统中的其他节点使用,私钥个人保留
    • 钱包地址一般是使用公钥进行一系列运算得来的
    • 我们不明确区分钱包地址和公钥,可不准确的理解为
      • 公钥、钱包:账户账号
      • 私钥:账户密码
    • 公钥私钥还有一密码学特征:单一钥匙进行加密的数据必定且只能用另一钥匙进行解密
      • 加密:发送者使用接受者的公钥对内容加密,只有接受者才能用自己的私钥对内容解密,避免了发送路径上的数据泄露
      • 签名验证:更进一步,发出者使用自己的私钥对内容进行签名,将签名和内容发给接受者,接受者使用发送者的公钥对签名解密,发现签名和内容一致,说明发送路径上,内容未被篡改
  • 交易信息:每笔交易以之前的交易记录为基础,需要满足输入等于输出(其实是存在差额的,差额会作为节点的手续费)
    • 示例:A 账号要支付 6 比特币给 D 账号,则交易数据写为:
      • 输入:
        • 之前某笔交易的输出( B 账号支付 2 比特币给 A 账号)
        • 之前某笔交易的输出( C 账号支付 3 比特币给 A 账号)
        • 之前某笔交易的输出( D 账号支付 2 比特币给 A 账号)
      • 输出:
        • 支付 6 比特币给 D 账号
        • 支付 1 比特币给 A 账号
    • 特点:
      • 需要说明支出的来源
      • 说明的资金来源中的金额不能拆分,多出的部分可以通过找零的方式转给自己
  • 交易信息进链:
    • 分布式:
      • 网络上很多节点进行存储和数据认证
      • 发送节点向周围节点广播交易信息
      • 每个节点都保存了区块链的完整或部分数据
    • 有效性:如某账号同时发送了多条矛盾的交易信息
      • 全节点将收到的交易信息进行验证并写入区块
      • 验证过程会使用到交易信息中的脚本
        • 节点发出的交易信息包含
          • 对资金来源的说明
          • 对每笔资金来源的解锁信息
        • 接收到信息的节点:
          • 确认 UTXO:验证资金来源是否存在 UTXO 中,即是否未使用
          • 执行脚本:
            • 将解锁签名和对应交易输出包含的锁定脚本拼接后以栈结构执行
            • 以示例信息为例子,拼接为 [私钥签名, 公钥](解锁脚本提供)[OP_DUP, OP_HASH160, 公钥哈希, OP_EQUALVERIFY, OP_CHECKSIG](锁定脚本提供)
            • 执行大致为:
              • 将公钥取出(OP_DUP)
              • 进行哈希 160 运算(OP_HASH160)
              • 将结果与公钥哈希对比是否相等(OP_EQUALVERIFY)
              • 验证签名(OP_CHECKSIG)
          • 确权:如何确认 UTXO 的所有权
            • 脚本执行过程中的将解锁脚本中公钥哈希后与锁定脚本中公钥哈希对比的过程即是确定所有所有权的操作
            • 确认了提供的银行卡账号确实是有这笔余额的账号
          • 交易信息防伪:
            • 发送节点:
              • 对交易信息哈希获得一个摘要
              • 用发送节点的私钥对摘要加密生成签名
              • 将内容和签名进行广播
            • 接收节点:执行 OP_CHECKSIG 的过程
              • 使用发送节点的公钥对签名解密得到摘要 1
              • 对内容进行哈希,得到摘要 2
              • 摘要 1 等于摘要 2 则公钥、私钥匹配,内容可信
              • 即交易信息是解锁这个交易输出的公钥的对应私钥拥有者写的
    • 一致性:网络传输问题,每个节点收到的交易信息可能不同,那么他们验证并写入区块的交易可能不同,如果都要上链则会造成最终生成的区块信息不一致
      • PoW:使用共识规则让单位时间内(约10分钟)只有一个节点有合法发布区块的权力,即获得了记账权
      • 规则:
        1. 将新区块的内容(前一个区块头哈希、这个区块的基本信息、新区块的交易信息根哈希)组合成字符串
        2. 在新区块字符串的末尾加上一个随机数,进行SHA-256,如果结果的二进制前 72 位全是 0 则工作完成,这是一个工作量巨大的碰运气工作
        3. 随着整体算力的提高会提升难度(增加 0 的数量等)将出块时间稳定在 10 分钟
      • 这个过程又因此被称为挖矿,需要大量的算力,进行 PoW 的节点又称为矿工
      • 节点收入:
        • 比特币网络奖励:节点在出块的时候会写入对自己地址进行 6.25 个比特币输出的奖励交易(Coinbase Newly Generated Coins)信息,此奖励每过 21 万个区块减半,也是比特币的发币方式
        • 区块中的交易手续费:区块中所有输入和输出金额的差值,由矿工写给自己
    • 分叉:凑巧同时生成了两个区块,则两个块都会并列入链,形成临时分叉,在后续计算中采用最长链原则,废弃追加区块数量少的分叉
    • 交易确认:正是因为分叉和废块的存在,所以交易一般需要在继续追加多个区块后才会被确认,比特币一般需要经过 6 个区块的追加才算是相对安全
    • 篡改:需要有超高的算力,篡改者的算力需要超过整个网络的算力才能实现篡改,才会使得自己的篡改分叉变成最长链,完成篡改

总结

  1. 使用 SHA-256 哈希算法和非对称加密制作数字签名进行防伪
  2. 执行交易信息中的脚本验证验证签名
  3. 使用区块链的区块存储比特币交易信息,使用哈希树存储并防止交易数据篡改
  4. 利用额外的工作规则达成网络一致性(拼算力)
  5. 将网络奖励和交易手续费支付给矿工节点

以太坊

以太坊黄皮书

https://ethereum.github.io/yellowpaper/paper.pdf

以太坊同样是一个交易驱动的状态机,它支持智能合约,是可编程的区块链,一种去中心化应用的平台。

我们看到比特币有一定的脚本执行能力,但是其脚本比较简单,只支持几种固定的交易协议和脚本命令。以太坊则不同,其是图灵完备的,区块链开发者可在支持范围内自由编程。

结构

区块头

  • parentHash:前一区块区块头计算哈希
  • ommersHash:叔块哈希
  • beneficiary:出块奖励的账户地址
  • stateRoot:状态树根哈希
  • transactionsRoot:交易树根哈希
  • receiptsRoot:收据树根哈希
  • logsBloom:数据布隆过滤器
  • gasLimit:区块汽油费上限,由矿工们共同维护

ommersHash

由于以太坊的出块速度较快,它出现临时分叉的可能性也就越高,ommersHash 正是出于抑制分叉的目的设计的,思路如下,被生效区块写入 ommersHash (最多写入两个)的废块也可以获得出块奖励,第一级是 7/8,第二级是 6/8,生效区块写入一个废块也会有 1/32 的奖励。

叔块

布隆过滤器

布隆过滤器是一种比较高效但是不太准的查找算法,大致机制如下:

  1. 提供一批哈希函数
  2. 将输入映射到阵列的几个点上
  3. 查询时查询阵列对应的点是否都有值即可

如:

  • 存:
    • apple 映射到点 1、7、9
    • banana 映射到点 1、3、7
  • 查:
    • orange 映射值为 1、3、9,都有值,则 orange 可能存在
    • peach 映射值为 2、3、7,2 无值,则 peach 一定不存在

可以看到布隆过滤器对存在的判断是会误报的,但是不存在的一定不会误报,所以适合大数据量的过滤。

区块体

与比特币不同的是,以太坊的区块体中有三棵树:状态树、交易树、收据树,树的数据会被全节点维护在本地数据库中。

  • 状态树:
    • 一种变种压缩前缀哈希树(Merkle Patricia Tree,MPT)
    • 存储了所有已知(即发生过交易的)的账户的状态
      • 外部账户:即钱包地址,无代码,能转账或执行智能合约
      • 合约账户:创建合约时生成,存有代码,能被触发执行智能合约
      • 账户状态:
        • nonce:地址的交易数量或合约数量
        • balance:余额
        • codeHash:合约账户的代码的哈希,外部账户没有
        • storageRoot:账户内容( 也是一棵 MPT 树)的根哈希
    • 新区块只会实际存储发生变化的账户的新状态,未发生变化的会指向之前的区块状态树的对应位置
  • 交易树:驱动状态树发生变更,也是 MPT,只保存区块中发生的交易信息
    • 消息调用的信息
    • 合约账户创建的信息
  • 收据树:也是 MPT
    • 交易执行过程中的特定信息编码为交易数据,保存在一个索引为键的树中
    • 交易过程中创建的日志会构成区块头的 Bloom 过滤器

共识机制

目前以太坊也是使用 PoW (工作量证明)即挖矿的方式确定记账权。

由一个 seed 生成一个小数据集(MB 级别),再由小的数据集生成一个大的数据集(GB 级别大小),在使用区块头和一个 nonce 值求取哈希,映射至大数据集,读取大数据集中目标位置和相邻元素,再哈希,循环 64 次,算出一个哈希和挖矿目标值对比。失败则更换区块头中 nonce 重新计算。

同时以太坊也有像 PoS 机制转换的计划,在以太坊的难度调节机制中存在一个难度因子,此难度因子每 10万个区块会翻一倍,是一个指数级的因子,所以又称为难度炸弹,由于 PoS 机制的上线不顺利,而难度炸弹导致出块速度提升过快,所以以太坊修改了其代码,使得难度因子计算时减去了 300 万个区块,也是由于代码升级导致了以太坊网络的一次硬分叉,还好这次分叉网络节点都进行了代码升级。

智能合约

前面提到,以太坊网络中存在合约账户,账户内存有一段代码,所以智能合约就是按照既定逻辑执行的代码。

调用

外部账户调用合约账户,外部账户发起的合约账户调用也可以调用另一个合约账户。

外部账户调用时,将目标函数和参数写在 data 域中,向合约账户发起交易。

合约调用合约的一种方式:

contract A {
    event LogCallFoo (string str);
    function foo (string str) returns (uint) {
        emit LogCallFoo(str);
        return 123;
    }
}

contract B {
    uint ua; // callAFooDirectly 的执行返回值
    function callAFooDirectly (address addr) public {
        A a = A(addr);
        ua = a.foo("call foo directly); // 调用 a 地址的 foo 方法
    }
}

创建和运行

  • 智能合约代码编写(solidity 等)完之后,编译成 bytecode
  • 创建:外部账户向 0*0 地址发起交易,将代码放在 data 域中
  • 智能合约运行在 EVM 中
  • 调用智能智能合约的交易将函数和参数写在交易的 data 域中,发布到区块链上后,受到消息的矿工则会按照参数执行代码
  • 发起调用的账户需要支付汽油费 GasLimit
    • 执行前全额扣除,开始执行,多退少补,不够则状态回滚,汽油费不退
    • 执行出错则整体状态回滚,汽油费不退:一个不严谨的例子,智能合约能发起了向 A、B 的转账,如果向 B 的转账出错了,向 A 转账的状态也会回滚
  • 每个交易执行之后形成一个收据,存有执行结果等信息

一个例子

这是一个拍卖出价的智能合约:

contract SimpleAuctionV1 {
    address public beneficiary; // 受益人
    uint public auctionEnd; // 结束时间
    address public highestBidder; // 当前最高出价地址
    mapping(address => unit) bids; // 所有出价
    address[] bidders; // 所有出价地址
    bool ended; // 是否结束

    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, unit amount);

    constructor(uint _biddingTime, address _beneficiary) public {
        beneficiary = _beneficiary;
        auctionEnd = now + _biddingTime
    }
}

// 参与拍卖的地址向此合约地址发起交易并支付货币
function bid() public payable {
    require(now <= auctionEnd);
    require(bids[msg.sender]+msg.value > bids[highestBidder]);
    // 没出过价则把出价人存起来
    if (!(bids[msg.sender] == unit(0))) {
        bidders.push(msg.sender);
    }
    // 如果出价最高则修改当前最高出价人
    highestBidder = msg.sender;
    bids[msg.sender] += msg.value;
    emit HighestBidIncreased(msg.sender, bins[msg.sender]);
}
// 拍卖结束
function auctionEnd()public {
    require(now > auctionEnd);
    require(!ended);
    // 把最高出价转给受益人
    beneficiary.transfer(bids[highestBidder]);
    // 给没竞拍成功的人退钱
    for (uint i = 0; i< bidders.length; i++) {
        address bidder = bidders[i];
        if (bidder == highestBidder) continue;
        bidder.transfer(bids[bidder]);
    }

    ended = true;
    emit AuctionEnded(highestBidder, bids[highestBidder]);
}
  • 这里存在一个问题:beneficiary.transfer,如果 beneficiary 是一个无法接受支付的合约地址(未声明 payable 关键词),那么会导致 auctionEnd 执行失败,导致所有出价锁在智能合约地址中,永远无法取出
  • 除此之外还有重入攻击,即调用的合约账户可能会反过来调用,引发循环执行

Code Is Law。

智能合约的逻辑由代码决定,已发布的合约代码无法修改,如果发布新的代码则会生成另一个合约账户地址,所以就算有 Bug 也无法修改。

The DAO(Decentralized Autonomous Organization):利用了重入攻击,黑客在自己的合约账户得收款函数中写了向 The DAO 发起调用的代码,使用循环调用的方式,转走了 5000万/1.5亿美元的以太币,约 10% 的以太坊系统总量的以太币,为了弥补对以太坊稳定性的重大影响,经过软分叉修复失败,以太币质押投票,以太坊选择了硬分叉的方式,将 The DAO 中的以太币强行转出,造成了社区分裂,造成以太坊硬分叉为 ETH、ETC。

NFT

从智能合约的例子中也可以看到,智能合约的开发者可以在合约账户的内部维护一个状态树。

其中的一种应用方式就是基于智能合约在区块链的网络内发行 Token,将所有持有人的状态维护在合约账户中。

NFT 正是这样的一种应用,全称为非同质化代币。NFT 的一大特征是,基于区块链的特性公开且防篡改的确定了某地址对某个数字作品的所有权。

一个 ERC721 标准(一种以太坊上发行 NFT 的标准)的 NFT 在区块链的数据示例:

  • 地址:0x40468d172cE354d3fAdAb06D0FA53eDdB2F3AA45
  • id:6
  • tokenURI:https://Opensea-creatures-api.herokuapp.com/api/creature/6)

由于 https 的内容会被修改,tokenURI 一般会用 IPFS 代替(分布式文件存储系统)。这个示例表明某地址拥有编号为 6 的某个物品,其内容为 tokenURI 的内容。

大多数 NFT 的实现上看,在区块链上只是记录了一个作品拥有者的地址、作品的编号、以及这个作品的链接,而作品本身在链下。所以也出现过现实中艺术家的作品被他人拿走铸造成 NFT 出售的情况,所以 NFT 不能解决线下的问题。从理论上讲,NFT 会提供数字资产的流通性,但是不能给物品本身赋予价值

通过调用智能合约的函数,可以进行 NFT 的转移、交易。所以可以知道,NFT 不单指某一个代币,而是一种代币类型,你也可以通过创建自己的智能合约,发行自己的 NFT 或 NFT 平台。

元宇宙

区块链应该会是元宇宙实现的一个技术,作为可任意复制的数字世界的数字资产进行确权的基础,如现有的 Decentraland 的虚拟土地、创造品的售卖。

同时,对于在数字世界中诞生的虚拟物品,由于不需要解决线下问题,NFT 的应用性应该会更高一些。

其他

  • 空块:在部分区块链中,新区块产生后,会先广播区块头再广播区块体,由于区块体数据较大,下载需要时间,部分矿工节点会在收到区块头后就开始挖矿,由于向新区块体中写入的交易信息不能与已有区块冲突或重复,所以为保证在未拿到前一区块信息的情况下的新区块的有效性,不向新区块体中写入实际交易信息(奖励信息除外),从而产生空块。

总结

  • 区块链存在交易慢、耗能高诟病,但要结合场景观测,在具体的场景下是比现有的解决方案快的。
  • 从理论上讲,基于区块链的 NFT 数字资产交易会提供数字资产的流通性,但是不能给物品本身赋予价值。
  • 投资有风险,追逐热点要谨慎。

参考资料

  • https://www.bilibili.com/video/BV1Vt411X7JF
  • https://www.youtube.com/watch?v=obRzfcvMshM&feature=youtu.be
  • https://blog.csdn.net/vigor2323/article/details/122711205

Content