<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>explorer_of_web3</title>
        <link>https://paragraph.com/@explorer-of-web3</link>
        <description>元宇宙新著民致力研究web3
会定期分享web3技术
公众号：web3探索者</description>
        <lastBuildDate>Tue, 28 Jul 2026 11:16:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[创作端角度聊一聊创作者经济]]></title>
            <link>https://paragraph.com/@explorer-of-web3/y0ZR9xe11qi2ttTVoeca</link>
            <guid>y0ZR9xe11qi2ttTVoeca</guid>
            <pubDate>Sat, 17 Dec 2022 13:07:45 GMT</pubDate>
            <description><![CDATA[创作者经济创作者经济是指通过支持创作者的方式来促进经济增长和发展的一种经济模式。在这种模式下，创作者通过创作内容（如音乐、文章、视频等）来获得报酬，从而为经济增加价值。创作者经济的核心理念是通过支持创作者来激发创造力和创新，并从中获益。 创作者经济可以通过多种方式来实现，比如通过版权保护和支付创作者的费用来支持创作者，或者通过提供便利的工具和平台来帮助创作者分发和出售自己的作品。 随着互联网的发展，创作者经济也变得越来越重要。互联网提供了一个全球性的平台，让创作者可以将自己的作品展示给更多的人，并获得更多的支持。同时，互联网也为创作者提供了更多的工具和平台，让他们可以更加轻松地创作和分发作品。 总之，创作者经济是一种支持创作者，促进创新和经济增长的经济模式，它在当今数字化时代具有越来越重要的作用。创作者如何获得收益创作者获得收益的方式有很多种，常见的方式有：通过版权保护和版税来获得报酬。在这种情况下，创作者通过登记版权，并与出版商或其他机构签订合同，在其作品被使用或复制时获得报酬。通过提供付费内容来获得报酬。在这种情况下，创作者可以通过提供付费内容，让用户通过支付费用来访问或下载...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">创作者经济</h2><p>创作者经济是指通过支持创作者的方式来促进经济增长和发展的一种经济模式。在这种模式下，创作者通过创作内容（如音乐、文章、视频等）来获得报酬，从而为经济增加价值。创作者经济的核心理念是通过支持创作者来激发创造力和创新，并从中获益。</p><p>创作者经济可以通过多种方式来实现，比如通过版权保护和支付创作者的费用来支持创作者，或者通过提供便利的工具和平台来帮助创作者分发和出售自己的作品。</p><p>随着互联网的发展，创作者经济也变得越来越重要。互联网提供了一个全球性的平台，让创作者可以将自己的作品展示给更多的人，并获得更多的支持。同时，互联网也为创作者提供了更多的工具和平台，让他们可以更加轻松地创作和分发作品。</p><p>总之，创作者经济是一种支持创作者，促进创新和经济增长的经济模式，它在当今数字化时代具有越来越重要的作用。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">创作者如何获得收益</h4><p>创作者获得收益的方式有很多种，常见的方式有：</p><ul><li><p>通过版权保护和版税来获得报酬。在这种情况下，创作者通过登记版权，并与出版商或其他机构签订合同，在其作品被使用或复制时获得报酬。</p></li><li><p>通过提供付费内容来获得报酬。在这种情况下，创作者可以通过提供付费内容，让用户通过支付费用来访问或下载内容。</p></li><li><p>通过广告收入来获得报酬。在这种情况下，创作者可以通过在其作品中植入广告，或者将其作品提供给广告商，通过广告收入来获得报酬。</p></li></ul><p>创作者获得的净收益=创作收益-创作成本-分发成本</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">创作成本</h4><p>创作者的创作成本可以分为两类：直接成本和间接成本。</p><p>直接成本是指创作者在创作过程中必须支付的费用，包括材料费用、工具费用、劳务费用等。例如，一个音乐家在创作一首歌曲时，必须购买乐器和录音设备，支付工作室的租金，以及雇佣录音师等人员，这些费用都属于直接成本。</p><p>间接成本是指创作者在创作过程中必须支付的非物质性费用，包括时间成本、精力成本、压力成本等。例如，一个音乐家在创作一首歌曲时，必须花费大量的时间来调试乐曲、录制歌曲、宣传歌曲等，这些费用都属于间接成本。</p><p>抖音的出现使得创作者可以轻松地利用手机拍摄短视频，而无需投入太多的成本。例如，在传统的电视制作中，制作一部短片需要拍摄设备、灯光设备、道具、服装等大量的物质成本，这对创作者来说是一笔巨大的投入。但在抖音上，创作者只需要拥有一部手机，就可以进行短视频创作。</p><p>此外，抖音还提供了丰富的编辑工具，让创作者可以通过手机轻松地剪辑视频。抖音的编辑工具包括滤镜、音乐、动效等多种功能，可以帮助创作者快速制作出专业水平的短视频。因此，抖音的出现降低了创作者的成本，使得创作者可以轻松地制作出高质量的短视频。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">分发成本(交易成本)</h4><p>创作的分发成本是指创作者在将作品分发给消费者时所需支付的费用。这些费用可能包括推广费用等。例如，一个音乐家在将作品提供给唱片公司时，必须支付推广费用，抖音创作者希望被更多人看到需要投放抖加等，这些费用都属于创作的分发成本。</p><p>创作的分发成本对创作者来说是一笔不可忽视的投入。如果创作者支付的分发成本过高，则可能会影响作品的销售收益，甚至导致创作者无法获得合理的报酬。因此，创作者应该选择合适的分发渠道，并优化分发成本，以确保自己的作品能够获得最大的收益。</p><p>随着互联网的发展，创作的分发成本也在不断降低。互联网提供了大量的分发渠道，让创作者可以通过网络将作品分发给消费者，而无需支付过多的分发成本。</p><h4 id="h-mcn" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">MCN为什么会出现</h4><p>MCN机构是一种专门为在线视频作者提供联合运营服务的机构。MCN机构通常由大量的在线视频作者组成，通过联合运营，为作者提供技术支持、内容创作、推广服务等。</p><p>MCN机构之所以会出现，是因为在线视频作者面临着许多挑战。例如，在线视频作者通常独立运营自己的频道，缺乏专业的技术支持和内容创作能力，难以提高作品的质量和受众群体。此外，在线视频作者还面临着激烈的竞争，难以吸引到足够的观众，并获得足够的收益。</p><p>为了解决这些问题，在线视频作者可以通过加入MCN机构，获得专业的技术支持和内容创作服务，提高作品的质量和吸引力。通过MCN机构的联合运营，在线视频作者还可以共享流量和广告收益，从而获得更多的收益。因此，MCN机构会出现，是为了为在线视频作者提供更好的服务，并帮助他们解决创作过程中遇到的困难。</p><p>引入MCN后创作者收益应该是 净收益=(创作收益-创作成本-分发成本) x 分成比</p><p>创作者加入MCN是希望增加创作收益，降低创作成本和分发成本，但最终净收益需要与MCN机构分成。</p><h2 id="h-web3" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">web3如何赋能创作者经济</h2><p>Web3技术可以通过两个角度来帮助创作者获得更多的经济收益。</p><p>首先，Web3技术可以通过NFT和分成协议来改善创作者创作初期负担成本过高的问题，NFT持有者将与创作者共担成本。而分成协议则可以用来保证创作者和NFT持有者之间能够获得更公平、更透明的收益分配。</p><p>其次，Web3技术还可以通过DAO来帮助创作者更好地组织多人共创。DAO可以让创作者们通过区块链技术来制定和执行共识规则，可以让创作者更好地协作、沟通和组织，从而提高创作效率和质量。通过DAO，创作者还可以更好地组织自己的社区，与粉丝和支持者进行互动，建立更牢固的创作生态。</p><h4 id="h-nft" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">分成协议+NFT</h4><p>可以想象作品NFT的持有者通过持有NFT可以获得创作者作品的收益分成，这一切都是公开透明的。那么，创作者将在创作前期获得更多的成本投入，产生优质内容的可能性也大大增加。这种方式和众筹的本质区别是NFT持有者是通过对创作者做投资，而非单纯的为爱发电，更加符合市场规律。</p><p>NFT持有者投资收益由NFT的分红属性和流动性决定。</p><h5 id="h-" class="text-lg font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">分红属性</h5><p>NFT的分红属性指的是，NFT持有者可以从作品产生的收益中获得分成。通常情况下，NFT的分红属性会通过智能合约来实现，这样可以确保分红过程的公平性和透明度，并且可以避免人为操作带来的错误和滥用。例如，在创作者的作品获得收益时，创作者和NFT持有者都可以从中获得一定比例的收益。这样就可以保证创作者能够从作品的成功中获得持续的收益，而NFT持有者也能够从投资的价值增长中获得回报。</p><h5 id="h-" class="text-lg font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">流动性</h5><p>NFT的流动性指的是，NFT可以在市场上流通，并且可以被不同的持有者购买和出售。NFT的流动性取决于市场需求和供给，以及NFT的价格波动程度等因素。如果NFT市场需求较大，流动性就会更高，这意味着NFT持有者可以更快速和方便地将NFT出售给其他投资者，从而获得相应的收益。反之，如果NFT市场需求较小，流动性就会较低，这意味着NFT持有者可能需要花费更长的时间才能找到愿意购买NFT的投资者。因此，NFT的流动性对于投资者而言是非常重要的，可以帮助他们评估投资风险并做出决策。</p><h5 id="h-" class="text-lg font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">分成协议</h5><p>智能合约可以用来实现分成协议，允许创作者与NFT持有者之间进行自动化的分配。智能合约使得分配过程更加快捷、透明、安全。在这种协议下，创作者可以指定分配比例，并且智能合约会自动按照规定的比例将收益分配给相应的参与者。这样可以确保分配过程的公平性和透明度，并且可以避免人为操作带来的错误和滥用。</p><p>下面是1个分成协议的简单例子</p><pre data-type="codeBlock" text="pragma solidity ^0.5.0;

contract Split {
    // 存储参与方的地址
    address[] public participants;

    // 存储总收益
    uint public total;

    // 存储每个参与方获得的分成比例
    mapping (address =&gt; uint) public shares;

    // 新增一个参与方，并分配分成比例
    function addParticipant(address _participant, uint _share) public {
        // 将参与方的地址添加到数组中
        participants.push(_participant);
        // 存储参与方的分成比例
        shares[_participant] = _share;
    }

    // 添加资金
    function addFunds(uint _amount) public {
        total += _amount;
    }

    // 分配收益，简单演示，智能合约分收益一般要自己取
    function distribute() public {
        // 计算每个参与方应该获得的金额
        for (uint i = 0; i &lt; participants.length; i++) {
            // 获取参与方地址
            address participant = participants[i];
            // 计算参与方应该获得的金额
            uint share = total * shares[participant] / 100;
            // 发放金额
            participant.transfer(share);
        }
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.5.0;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Split</span> </span>{
    <span class="hljs-comment">// 存储参与方的地址</span>
    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">public</span> participants;

    <span class="hljs-comment">// 存储总收益</span>
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> total;

    <span class="hljs-comment">// 存储每个参与方获得的分成比例</span>
    <span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">uint</span>) <span class="hljs-keyword">public</span> shares;

    <span class="hljs-comment">// 新增一个参与方，并分配分成比例</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addParticipant</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _participant, <span class="hljs-keyword">uint</span> _share</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// 将参与方的地址添加到数组中</span>
        participants.<span class="hljs-built_in">push</span>(_participant);
        <span class="hljs-comment">// 存储参与方的分成比例</span>
        shares[_participant] <span class="hljs-operator">=</span> _share;
    }

    <span class="hljs-comment">// 添加资金</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addFunds</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        total <span class="hljs-operator">+</span><span class="hljs-operator">=</span> _amount;
    }

    <span class="hljs-comment">// 分配收益，简单演示，智能合约分收益一般要自己取</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">distribute</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// 计算每个参与方应该获得的金额</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> participants.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-comment">// 获取参与方地址</span>
            <span class="hljs-keyword">address</span> participant <span class="hljs-operator">=</span> participants[i];
            <span class="hljs-comment">// 计算参与方应该获得的金额</span>
            <span class="hljs-keyword">uint</span> share <span class="hljs-operator">=</span> total <span class="hljs-operator">*</span> shares[participant] <span class="hljs-operator">/</span> <span class="hljs-number">100</span>;
            <span class="hljs-comment">// 发放金额</span>
            participant.<span class="hljs-built_in">transfer</span>(share);
        }
    }
}
</code></pre><h4 id="h-dao" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">DAO</h4><p>在DAO中，创作者可以根据自己的需求和意愿来参与创作过程，并且通过智能合约来实现贡献的评估和激励。这种方式可以有效地促进创作者之间的协作和交流，并且可以为创作者提供更多的自主权和参与感。此外，DAO还可以通过共同持有NFT来实现更高效的分配和激励，这样就可以保证创作者能够从作品的成功中获得持续的收益。DAO也可以通过提供项目信息、交易历史、投票记录等内容来增强透明度，从而为创作者和投资者提供更多的信息参考。</p><p>如果一群电影制作人想要使用DAO的形式进行创作，他们可以创建一个智能合约来管理项目的资金和决策流程。成员可以通过投票来决定电影的故事情节、演员阵容和制作细节，并可以通过智能合约来监督执行情况和跟踪资金使用。这样，制作人就可以在去中心化的环境中实现协作和决策，而无需依赖于中心化的组织机构。</p><p>但创作需要构思的完整性，如何在创作构思主线时运行DAO仍然是1个比较大的挑战，现在更多的应用是在分支剧情上引入DAO进行投票选择或创作。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>越来越多的人会成为创作者，而web3为我们带来了很多无需信任即可合作的基础设施，这也使得合作更容易产生，而更多的合作，将会产生相比1个人更加优质的内容。这是我所坚信的。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
        </item>
        <item>
            <title><![CDATA[将优惠券引入NFT的一次思考]]></title>
            <link>https://paragraph.com/@explorer-of-web3/nft-3</link>
            <guid>I32LGZfmMANO3Z2FYXO0</guid>
            <pubDate>Mon, 03 Oct 2022 02:10:55 GMT</pubDate>
            <description><![CDATA[优惠券概述现在各大电商平台都有着各种各样的优惠券，合理的发放优惠券可以达到2个目标：促进用户进行消费，提高GMV。通过优惠券作为奖励可以促进用户完成一些任务，带来DAU，留存以及时长的增长。相比发放优惠券，直接发放货币应当有更加明显的效果，但成本也会相应增加,优惠券的优势主要体现在3个方面:使用范围收窄，一般而言优惠券的使用会增长诸多限制，比如某个平台，某种品类，满X减Y等等。这就使得优惠券促进用户购买指定商品的能力大大提升。流通性大幅降低，货币的流通性无疑是非常高的，但优惠券默认具有不可转让属性，以及因为不同用户具有不同的消费需求，所以优惠券几乎没有流通性，这就使得优惠券只有在购买商品时才具有价值。成本滞后性，发放优惠券的成本发生在用户使用优惠券购买商品时，而发放货币的成本发生在发放时。当然，在NFT场景下增加优惠劵，和NFT本身是消费商品还是投资品也有关系，作为投资品的NFT，本身流通性较高，消费劵本身没有流通性，但购买完成投资品NFT时就被关联了流通性。 此外，优惠券还分为平台发放和卖出方发放:平台发放优惠券时优惠行为对卖出方无感知，卖出商品后卖出方还是会收到原始售价的货币...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">优惠券概述</h2><p>现在各大电商平台都有着各种各样的优惠券，合理的发放优惠券可以达到2个目标：</p><ol><li><p>促进用户进行消费，提高GMV。</p></li><li><p>通过优惠券作为奖励可以促进用户完成一些任务，带来DAU，留存以及时长的增长。</p></li></ol><p>相比发放优惠券，直接发放货币应当有更加明显的效果，但成本也会相应增加,优惠券的优势主要体现在3个方面:</p><ol><li><p>使用范围收窄，一般而言优惠券的使用会增长诸多限制，比如某个平台，某种品类，满X减Y等等。这就使得优惠券促进用户购买指定商品的能力大大提升。</p></li><li><p>流通性大幅降低，货币的流通性无疑是非常高的，但优惠券默认具有不可转让属性，以及因为不同用户具有不同的消费需求，所以优惠券几乎没有流通性，这就使得优惠券只有在购买商品时才具有价值。</p></li><li><p>成本滞后性，发放优惠券的成本发生在用户使用优惠券购买商品时，而发放货币的成本发生在发放时。</p></li></ol><p>当然，在NFT场景下增加优惠劵，和NFT本身是消费商品还是投资品也有关系，作为投资品的NFT，本身流通性较高，消费劵本身没有流通性，但购买完成投资品NFT时就被关联了流通性。</p><p>此外，优惠券还分为平台发放和卖出方发放:</p><ul><li><p>平台发放优惠券时优惠行为对卖出方无感知，卖出商品后卖出方还是会收到原始售价的货币，购买者使用优惠券减价的部分由平台补齐。</p></li><li><p>卖出方发放优惠券时，卖出商品后卖出方收到的货币是使用过优惠券后价格的货币。成本由卖出方自行承担 。</p></li></ul><h2 id="h-nft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">NFT场景下融入优惠券</h2><p>NFT的流转主要分为2个阶段</p><ol><li><p>项目方发布NFT阶段，也就是mint阶段，此时用户直接和NFT合约进行交互，用户mint时给予NFT合约链上货币，NFT合约将NFT发放给用户的钱包地址。</p></li><li><p>交易市场交易阶段，在这个阶段NFT持有者和交易市场合约进行交互，暂时授予交易市场合约NFT的转让权，购买者通过与交易市场合约交互获取NFT。</p></li></ol><ul><li><p>在mint阶段融入优惠券，NFT发行方可以通过优惠券作为奖励促进项目前期的宣发。也可以通过优惠券来促进更多用户去mint。此时NFT发行方的成本是减少收入而不会额外支出。</p></li><li><p>在交易市场交易融入优惠券，会复杂一些。此时共有3方可以发放优惠券</p><ul><li><p>NFT发行方,因为此时NFT并不归属于NFT发行方而归属于当前NFT持有者，所以需要发行方设置1个特定钱包为优惠券买单。(卖出NFT的持有者收到的货币不会发生变化，优惠券发放钱包需补差额=NFT售出价格-买方使用优惠劵后的价格)。此时NFT发行方需要额外支出。</p></li><li><p>NFT持有者, NFT持有者发放优惠券时，需要NFT交易市场支持设置优惠券合约信息。在购买时校验优惠券直接进行优惠。不过目前来看专业NFT卖家很少，所以这种方式我们不做讨论。</p></li><li><p>NFT交易市场，交易市场需要设置特定钱包为优惠券买单。和NFT发行方基本一致。不过交易市场的优惠券可以做到覆盖整个交易市场中的所有NFT。此时NFT交易市场需要额外支出。</p></li></ul></li></ul><p>从上面的论述可以看出mint阶段NFT发行方发放优惠券不需要额外支出，而在交易市场交易阶段，NFT发行方或NFT交易市场发放优惠券时需要进行额外支出。当需要额外支出时，就需要仔细计算成本和收益。</p><ul><li><p>市场交易时NFT发行方发放优化劵，成本是所有发放的优惠券总额 x 优惠券使用率，收益是完成获得优惠券的任务以及该NFT更大的流通性。</p></li><li><p>NFT交易市场发放优惠券，带来的成本是所有发放的优惠券总额 x 优惠券使用率，收益是完成获得优惠券的任务以及平台产生更多的交易金额。交易市场更多的交易金额这个收益在传统电商平台很重要，因为交易市场通过吸引大量的用户可以获得卖家投放广告的收益。但在目前NFT交易市场来看，大家并不会逛NFT交易市场，更多的是目标明确的直接购买。另外上面也说到专业的NFT卖家很少，所以NFT交易平台也无法获得广告收益。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结论</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7cd35c0cb3ee1666f59130a5d3d77435e8cecc4c01e590081f948b649ef0052b.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>综上所述，在mint阶段NFT发行方发放优惠券，是比较合适的做法。目前NFT发行方前置宣传很多是通过社交平台抽奖+白名单的形式，引入优惠券后可以打破单一的概率性玩法。在市场交易阶段NFT发行方发放优惠券需要评估好成本和收益。而NFT持有者以及NFT交易市场发放优惠券看起来并不合适。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">后续</h2><p>本文是将优惠券引入NFT的一次产品层面上思考，后续会写一篇文章介绍如何完成优惠券合约以及修改当前的NFT合约以支持优惠券(当然前提是本人还认为这一产品思路可行，如果产品思路不可行，技术也不必实现)。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/78990dad7d1f250291ee2b2831a264da02ed92433c629e0be2b6f317146c1cc8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[starknet开发的一些建议]]></title>
            <link>https://paragraph.com/@explorer-of-web3/starknet</link>
            <guid>RaC8nSVY69eW75TnywnA</guid>
            <pubDate>Sun, 05 Jun 2022 06:31:03 GMT</pubDate>
            <description><![CDATA[最近在使用starknet进行开发，这里提供一些starknet的开发建议，希望对大家有所帮助。不要使用warp(至少目前阶段不要使用)因为starknet使用了新的开发语言cairo，所以对以太坊solidity合约无法直接迁移。新语言cairo不仅仅是开发语言的转变，而且整个开发工具链都发生了变化，其原因是cairo属于python体系所以工具链都采用了python,而solidity的工具链大都使用js。所以开始想到的是，是否存在办法可以将solidity合约直接转为cairo，这样dapp的迁移成本会大大降低。https://github.com/NethermindEth/warp正是这样一个项目。但经过尝试发现warp转换过的编译产物体积会膨胀非常严重，比如原来较为简单的nft项目将无法部署。此外由于cairo不支持msg.value等(后面会介绍不支持的原因)以及对字符串的不友好导致我们无法通过简单的转换工具来迁移合约变得不可行，所以目前阶段建议大家不要通过warp转换solidity的方式进行迁移，而是直接使用cairo进行重写。使用docker搭建纯粹的开发环境...]]></description>
            <content:encoded><![CDATA[<p>最近在使用starknet进行开发，这里提供一些starknet的开发建议，希望对大家有所帮助。</p><h3 id="h-warp" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">不要使用warp(至少目前阶段不要使用)</h3><p>因为starknet使用了新的开发语言cairo，所以对以太坊solidity合约无法直接迁移。新语言cairo不仅仅是开发语言的转变，而且整个开发工具链都发生了变化，其原因是cairo属于python体系所以工具链都采用了python,而solidity的工具链大都使用js。所以开始想到的是，是否存在办法可以将solidity合约直接转为cairo，这样dapp的迁移成本会大大降低。<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/NethermindEth/warp">https://github.com/NethermindEth/warp</a>正是这样一个项目。但经过尝试发现warp转换过的编译产物体积会膨胀非常严重，比如原来较为简单的nft项目将无法部署。此外由于cairo不支持msg.value等(后面会介绍不支持的原因)以及对字符串的不友好导致我们无法通过简单的转换工具来迁移合约变得不可行，所以目前阶段建议大家不要通过warp转换solidity的方式进行迁移，而是直接使用cairo进行重写。</p><h3 id="h-docker" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">使用docker搭建纯粹的开发环境</h3><p>在官方指导中使用python venv来创建python的虚拟环境，venv主要用于依赖包的隔离，但并未解决python版本本身的问题，如果没有使用python3.7可能会在开发中遇到各种各样的问题，有什么办法可以获得较为纯粹的开发环境呢？这里建议使用容器化方案docker,首先我们创建1个Dockerfile，内容如下所示:</p><pre data-type="codeBlock" text="FROM python:3.7

RUN mkdir cairo-contracts
WORKDIR cairo-contracts
"><code>FROM python:<span class="hljs-number">3.7</span>

RUN mkdir cairo<span class="hljs-operator">-</span>contracts
WORKDIR cairo<span class="hljs-operator">-</span>contracts
</code></pre><p>Dockerfile非常简单，因为我们后续将在docker中进行开发，所以核心只需要From python:3.7即可(代表指定1个安装python3.7的基础镜像)其余命令都可以在进入这个容器后执行。vscode安装<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Remote - Containers</a>插件后可以支持打开docker中的文件</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cdb846046fe492b2aecf3f0d733eb99afb051d51ffa228335c58695dacaa1cf6.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>此时的开发环境就像是1台纯净的标准化Linux系统一样。</p><p>可以打开命令行对该容器进行一切Linux操作。 比如我们使用pip安装cairo-lang or nile-cairo (建议使用nile其依赖cairo-lang但更加好用)</p><pre data-type="codeBlock" text="pip install nile-cairo
or
pip install cairo-lang
"><code>pip install nile<span class="hljs-operator">-</span>cairo
or
pip install cairo<span class="hljs-operator">-</span>lang
</code></pre><p>这就是容器化的力量，docker这种容器化的方案减少了很多维护工作，前端开发同学可能接触比较少，但在后端docker和k8s已经成为标准。这里不再详细介绍，对docker和k8s有兴趣的可以自行查阅相关资料。</p><h3 id="h-cairofelt" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">了解cairo的核心类型felt</h3><p>felt是1个-P/2&lt;felt&lt;P/2的整数。目前P是252位的整数。</p><h4 id="h-uint256" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">uint256</h4><p>cairo中felt的取值范围和solidity中最常用的uint256不一致。在cairo中也为我们提供了uint256的结构，定义如下所示, cairo中的uint256是使用2个felt组合表示，分别表示高低128位。</p><pre data-type="codeBlock" text="struct Uint256:
    # The low 128 bits of the value.
    member low : felt
    # The high 128 bits of the value.
    member high : felt
end
"><code><span class="hljs-attr">struct Uint256:</span>
    <span class="hljs-comment"># The low 128 bits of the value.</span>
    <span class="hljs-attr">member low :</span> <span class="hljs-string">felt</span>
    <span class="hljs-comment"># The high 128 bits of the value.</span>
    <span class="hljs-attr">member high :</span> <span class="hljs-string">felt</span>
<span class="hljs-string">end</span>
</code></pre><h4 id="h-openzeppelin-erc721baseuritokenuri13" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">OpenZeppelin ERC721不支持baseURI以及tokenURI13个字符限制的原因</h4><p>在cairo中并未直接提供string类型。</p><p>首先可以回顾string在计算机中的存储方式，一般而言string需要进行ascii encode变为二进制，每个字符由8位二进制来表示，这是因为计算机只能存储0 1。我们平时因为各个开发语言都已经在底层帮我们封装完成所以不需要关注这一过程，但cairo并未做这样的封装，所以需要开发者来完成string的 decode 和encode。</p><p>在cairo中不能直接使用string，我们需要在string数据上链前进行ascii encode变为整数，然后在读取链上string数据的时候进行ascii decode变回string。这也使得在链上进行字符串拼接变得比较麻烦。</p><p>比如我们可以看到OpenZeppelin提供的ERC721实现中就不支持使用baseUri+tokenid的方式去设置我们的tokenUrl，其原因就是在使用felt存储为整数时进行字符串拼接不是那么方便（只能使用&lt;&lt;方式,但同时又要避免felt溢出的问题，处理起来会很麻烦）。</p><p>此外使用felt表示string 还需要注意每个flet只能表示252/8=31个字符，如果超过31个字符，则需要使用flet数组。在OpenZeppelin ERC721的文档<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/cairo-contracts/blob/main/docs/ERC721.md">https://github.com/OpenZeppelin/cairo-contracts/blob/main/docs/ERC721.md</a>中提到toknURI只支持31个字符，没有使用flet数组的原因是不符合ERC721规范，个人认为这里不太合理，因为31个字符很难表示1个tokneURI(除非使用短链接，但又会涉及到中心化服务的问题)。</p><h3 id="h-python" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">搭建与合约交互的python脚本</h3><p>cario提供了cli交互方式，但直接使用命令行交互，对于很多入参的前置处理会比较麻烦，比如上面说到cairo不支持string，如果我们需要存储1个string(比如nft的链接或合约name)就需要提前转换好之后再复制到命令行手动成本较高。所以我们使用python脚本去完成与合约的交互。下面分别就是进行部署1个nft合约以及mint操作时的pyhton脚本，内部还是使用了官方提供的cli。</p><pre data-type="codeBlock" text="import os
from utils import (str_to_felt)

name=str_to_felt(&quot;nft-web3-explorer-starknet&quot;)
symbol=str_to_felt(&quot;nft-web3-explorer-starknet&quot;)
account_address=&quot;合约账户地址&quot;
contract_abi=&quot;./artifacts/abis/nft-web3-explorer-starknet.json&quot;
os.system(&quot;starknet deploy  --contract %s --inputs %s %s %s&quot; % (contract_abi, name, symbol, account_address))
"><code><span class="hljs-keyword">import</span> <span class="hljs-title">os</span>
<span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">utils</span> <span class="hljs-title"><span class="hljs-keyword">import</span></span> (<span class="hljs-title">str_to_felt</span>)

<span class="hljs-title">name</span><span class="hljs-operator">=</span><span class="hljs-title">str_to_felt</span>(<span class="hljs-string">"nft-web3-explorer-starknet"</span>)
<span class="hljs-title">symbol</span><span class="hljs-operator">=</span><span class="hljs-title">str_to_felt</span>(<span class="hljs-string">"nft-web3-explorer-starknet"</span>)
<span class="hljs-title">account_address</span><span class="hljs-operator">=</span><span class="hljs-string">"合约账户地址"</span>
<span class="hljs-title">contract_abi</span><span class="hljs-operator">=</span><span class="hljs-string">"./artifacts/abis/nft-web3-explorer-starknet.json"</span>
<span class="hljs-title">os</span>.<span class="hljs-title">system</span>(<span class="hljs-string">"starknet deploy  --contract %s --inputs %s %s %s"</span> <span class="hljs-operator">%</span> (<span class="hljs-title">contract_abi</span>, <span class="hljs-title">name</span>, <span class="hljs-title">symbol</span>, <span class="hljs-title">account_address</span>))
</code></pre><pre data-type="codeBlock" text="import os

contract_address=&quot;合约地址&quot;
contract_abi=&quot;./artifacts/abis/nft-web3-explorer-starknet.json&quot;
function_name=&quot;mint&quot;
mint_account_address=&quot;mint合约账户地址&quot;
//因为定义token是uint256 所以要传2个值
token=&quot;1 0&quot;
os.system(&quot;starknet invoke  --address %s --abi %s --function %s --inputs %s %s  --account xxxxx&quot; % 
(contract_address, contract_abi, function_name, mint_account_address, token))
"><code>import os

<span class="hljs-attr">contract_address</span>=<span class="hljs-string">"合约地址"</span>
<span class="hljs-attr">contract_abi</span>=<span class="hljs-string">"./artifacts/abis/nft-web3-explorer-starknet.json"</span>
<span class="hljs-attr">function_name</span>=<span class="hljs-string">"mint"</span>
<span class="hljs-attr">mint_account_address</span>=<span class="hljs-string">"mint合约账户地址"</span>
//因为定义token是uint256 所以要传2个值
<span class="hljs-attr">token</span>=<span class="hljs-string">"1 0"</span>
os.system("starknet invoke  --address %s --abi %s --function %s --inputs %s %s  --account xxxxx" % 
(contract_address, contract_abi, function_name, mint_account_address, token))
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">了解自己的私钥存储在哪里</h3><p>在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/wallets/open_zeppelin.py">https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/wallets/open_zeppelin.py</a>中可以看到这个代码是在我们创建合约账户时指定的钱包，其作用就是来管理我们的账户。其实无论合约账户还是EOA都需要有私钥、公钥、地址这些必要组成。但在我们部署完合约账户时只在命令行中返回给我们地址，那其他信息存储在哪里呢？ 我们可以查看 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/cli/starknet_cli.py">https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/cli/starknet_cli.py</a> 内的相关逻辑</p><pre data-type="codeBlock" text="def get_account_dir(args) -&gt; str:
    &quot;&quot;&quot;
    Returns the directory containing the wallet files. By default, DEFAULT_ACCOUNT_DIR is used.
    &quot;&quot;&quot;
    value = get_optional_arg_value(
        args=args, arg_name=&quot;account_dir&quot;, environment_var=&quot;STARKNET_ACCOUNT_DIR&quot;
    )
    if value is None:
        return DEFAULT_ACCOUNT_DIR
    return value
"><code><span class="hljs-keyword">def</span> <span class="hljs-title function_">get_account_dir</span>(<span class="hljs-params">args</span>) -> <span class="hljs-built_in">str</span>:
    <span class="hljs-string">"""
    Returns the directory containing the wallet files. By default, DEFAULT_ACCOUNT_DIR is used.
    """</span>
    value = get_optional_arg_value(
        args=args, arg_name=<span class="hljs-string">"account_dir"</span>, environment_var=<span class="hljs-string">"STARKNET_ACCOUNT_DIR"</span>
    )
    <span class="hljs-keyword">if</span> value <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">return</span> DEFAULT_ACCOUNT_DIR
    <span class="hljs-keyword">return</span> value
</code></pre><p>可以看到如果我们在部署账户合约时没有指定--account_dir参数会使用默认值DEFAULT_ACCOUNT_DIR</p><pre data-type="codeBlock" text="DEFAULT_ACCOUNT_DIR = &quot;~/.starknet_accounts&quot;
"><code><span class="hljs-attr">DEFAULT_ACCOUNT_DIR</span> = <span class="hljs-string">"~/.starknet_accounts"</span>
</code></pre><p>在部署完成合约账户后我们可以使用命令查看我们合约账户的相关信息</p><pre data-type="codeBlock" text="cat ~/.starknet_accounts/starknet_open_zeppelin_accounts.json 
{
    &quot;alpha-goerli&quot;: {
        &quot;账户名字&quot;: {
            &quot;private_key&quot;: &quot;私钥&quot;,
            &quot;public_key&quot;: &quot;公钥&quot;,
            &quot;address&quot;: &quot;合约地址&quot;
        }
    }
}
"><code>cat <span class="hljs-operator">~</span><span class="hljs-operator">/</span>.starknet_accounts/starknet_open_zeppelin_accounts.json 
{
    <span class="hljs-string">"alpha-goerli"</span>: {
        <span class="hljs-string">"账户名字"</span>: {
            <span class="hljs-string">"private_key"</span>: <span class="hljs-string">"私钥"</span>,
            <span class="hljs-string">"public_key"</span>: <span class="hljs-string">"公钥"</span>,
            <span class="hljs-string">"address"</span>: <span class="hljs-string">"合约地址"</span>
        }
    }
}
</code></pre><h3 id="h-msgvalue" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">真正理解转账，消失了的msg.value</h3><p>我们在查看OpenZeppelin的ERC20合约以及ERC721合约时，会发现1个问题，这些合约之前的转账操作全部没了(所以OpenZeppelin的ERC721不能直接使用)，这样让我们对starknet上如何转账有了疑问。要回答这个问题，我们就必须了解L2的货币从哪里来，L2的流通token也是eth，使用bridge的方式进行转换。如下图所示:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5865e7bcafa3c1ab2d7b0591093cab0d69fa746d4ccc54311871ac2812e3e910.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>L1和L2是可以通过发消息来进行异步交互的，具体可见 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://starknet.io/docs/hello_starknet/l1l2.html">https://starknet.io/docs/hello_starknet/l1l2.html</a></p><p>bridge的完整代码大家可以查看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/starkgate-contracts">https://github.com/starkware-libs/starkgate-contracts</a></p><p>可以看到通过bridge，L1转到L2的ETH被存储到1个特定地址的ERC20中，我们进行转账操作就是对该ERC20合约调用transfer()方法。每个网络中都会有1个唯一的ERC20合约地址，我们需要通过这个合约地址来访问网络中通用token的相关信息。合约地址位于<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/starknet-addresses">https://github.com/starkware-libs/starknet-addresses</a>。</p><p>通过上面的分析可以看到在starknet中的转账是通过操作1个特定地址的ERC20完成，自然也就没有msg.value了。</p><p>一些参考链接:</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://voyager.online/">https://voyager.online/</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/cairo-lang/">https://github.com/starkware-libs/cairo-lang/</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/starkgate-contracts">https://github.com/starkware-libs/starkgate-contracts</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/starknet-addresses">https://github.com/starkware-libs/starknet-addresses</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/cairo-contracts">https://github.com/OpenZeppelin/cairo-contracts</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/NethermindEth/warp">https://github.com/NethermindEth/warp</a></p></li></ul>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c92ce77e83a680c761ba8fafb1a53c6facb55f5807eb7a47c9eaa76d08e4735e.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[去中心化存储那些事(下)]]></title>
            <link>https://paragraph.com/@explorer-of-web3/ylZrp2MV2BV4KfRj7bul</link>
            <guid>ylZrp2MV2BV4KfRj7bul</guid>
            <pubDate>Sun, 22 May 2022 07:49:19 GMT</pubDate>
            <description><![CDATA[上篇文章我们讨论了去中心化存储ipfs和filecoin。文末提到filecoin虽然提供了去中心化存储，但系统并未提供数据冗余的能力，也就意味着机制上并不保证存储在filecoin中的数据不会丢失，本文我们将讨论提供了冗余机制的去中心化存储解决方案-Arweave。ArweaveArweave实现了什么Arweave 实现了数据的永久存储。即一次进行付费存储，数据将会永久存储在Arweave网络中。Arweave如何实现适度冗余与 filecoin 链下存储的思路不同，Arweave 采用链上存储。在常规的区块链如以太坊中，每个全节点都会下载所有区块。这样来看链上存储可以解决之前提到 filecoin 没有冗余性的设计问题。但如果采用这种方式做去中心化存储也带来大部分节点存储不够的问题，因为每个节点将存储所有的数据，换句话说这样冗余度太高了，我们需要适度冗余即可。每个节点存储部分区块如何实现适度冗余呢？Arweave 为了解决这一问题设计了不同的区块链数据结构以及证明方式。每个节点只存储一部分区块。 Arweave使用2张表解决了节点必须存储所有区块的问题区块哈希列表钱包列表有...]]></description>
            <content:encoded><![CDATA[<p>上篇文章我们讨论了去中心化存储ipfs和filecoin。文末提到filecoin虽然提供了去中心化存储，但系统并未提供数据冗余的能力，也就意味着机制上并不保证存储在filecoin中的数据不会丢失，本文我们将讨论提供了冗余机制的去中心化存储解决方案-Arweave。</p><h1 id="h-arweave" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Arweave</h1><h2 id="h-arweave" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Arweave实现了什么</h2><p>Arweave 实现了数据的永久存储。即一次进行付费存储，数据将会永久存储在Arweave网络中。</p><h2 id="h-arweave" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Arweave如何实现</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">适度冗余</h3><p>与 filecoin 链下存储的思路不同，Arweave <strong>采用链上存储</strong>。在常规的区块链如以太坊中，每个全节点都会下载所有区块。这样来看链上存储可以解决之前提到 filecoin 没有冗余性的设计问题。但如果采用这种方式做去中心化存储也带来大部分节点存储不够的问题，因为每个节点将存储所有的数据，换句话说这样冗余度太高了，我们需要适度冗余即可。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">每个节点存储部分区块</h3><p>如何实现适度冗余呢？Arweave 为了解决这一问题设计了不同的区块链数据结构以及证明方式。每个节点只存储一部分区块。</p><p>Arweave使用2张表解决了节点必须存储所有区块的问题</p><ol><li><p>区块哈希列表</p></li><li><p>钱包列表</p></li></ol><p>有了块哈希列表，我们不需要下载所有区块进行逐个验证，而是在区块生成过程中持续验证。</p><p>有了钱包表，Arweave可以在当前区块即可查到任何一个账户的状态。其原因和以太坊中为何状态树不和交易树一样只存储当前区块涉及到的账户一样。即假设要给1个没有发生过交易的账户转账，则需要遍历到初始块中。以太坊的设计是为了提高效率，而Arweave则是为了实现只存储部分区块的目标。</p><h3 id="h-poablockwave" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">冗余实现-POA和blockwave</h3><p>如果只是这样Arweave解决了每个节点只存储部分区块，但无法保证区块的冗余性，因为极限情况下Arweave永远只需要存储最近的一个区块即可。Arwave采用POA服务证明来解决这一问题。</p><h4 id="h-poa" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">POA访问证明</h4><p>Arweave中采用POW+POA的方式确定出块权，POW我们之前的文章多次提过不再重复，POA是为了实现每个区块在不同节点的冗余性，其要求是节点出新块的时候需要证明存储了之前随机的某个区块-recall-blockk。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ee736f56189e9f9eea18850226f81a2afeeeaf9a5f9be19a0925874f3bbc4b7f.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>如上图所示，区块4生成的时候要求包含之前链上区块1的部分数据data2。这使得区块4需要存储区块1才可以参与后续挖矿。</p><h4 id="h-blockweave" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">blockweave</h4><p>由于上面POA的方式，Arweave的区块链结构变成了图，当前区块不仅与前一区块连接并且和历史某个随机区块相连接。如图所示</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8ede1a44f3b1662758d3e63a006f511f2af9034e555719029145588714987bfa.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>我们将上面这样的结构称之为blockweave。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">生成新块</h3><p>我们来描述Arweave生成1个新的区块的具体流程</p><ol><li><p>当面末尾区块是区块5，节点A存储了区块2和3</p></li><li><p>生成区块6的需要携带区块2的数据，节点A进行POW</p></li><li><p>通过比拼节点A POW竞争中胜出，获得记账权</p></li><li><p>节点A打包事务并按照要求存入区块2的部分数据，整体打包生成新的区块</p></li><li><p>节点A将新的区块和recall block广播给其他节点</p></li><li><p>其他节点验证新的区块是否有效，有效包含区块的随机数符合POW以及区块中是否包含recall block的部分数据。</p></li></ol><p>上面的过程，为什么要将recall block也一并广播给其他节点呢？Arweave每个节点只存储部分区块，所以很多节点并没有该recall block，但很多都包含recall block的hash值，这样我们可以确认传输过来的recall block的正确性，并用该recall block去验证新的区块中包含的数据是否正确。</p><p>Arweave通过POA的方式可以实现均匀的将块分布在不同节点上，每次新块需要的recall block是随机的，一旦某个区块有较少节点存储那么拥有这个区块的节点获得记账权的概率就会变大，从而其他节点也来存储该区块。而如果按照均匀分布计算，某个区块丢失的概率为 (1-平均每个节点存储的区块数/网络中所有区块)^网络中节点数，由于网络中节点数不断增加，区块丢失概率非常之低。</p><p>由此可见，Arweave的永久存储还是比较靠谱的，真正从机制上实现了存储冗余。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">分发机制</h3><p>存储的问题基本解释清楚了，之前说过存储系统有2个事情需要解决1个是存数据 1个是分发数据，我们简单看看Areave的数据分发如何</p><p>数据访问 Arweave使用了wildfire系统来保障数据更快分发，wildfire是一个排名系统，每个节点都会维护起对等点的排名，排名根据对等点的请求响应耗时来决定，会根据排名顺序来进行顺序交互，这样性能不佳的节点由于在每个节点排名都较差，最终没有节点与之交互从而被踢出网络。</p><h1 id="h-arweave-vs-filecoin" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Arweave vs filecoin</h1><p>同样作为去中心和的存储方案，我们来看看Arweave和filecoin有什么区别，这里不再对比ipfs，之前文章已经论述过ipfs是更侧重数据分发的协议而非存储方案,并且filecoin就是基于ipfs搭建的。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3a8ea6e6cfe5339da74a1178cdb80bb72099dee5aeff9227e2648a4c301d4ad7.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>这两种方式整体上看目前Arweave要相对更实用一些。filecoin核心问题在于其分发机制一直被诟病，基本上无法单独满足用户体验。</p><p>至此，我们对去中心化的存储讨论告一段落，但文中既然提到了Arweave，我们就来继续看看Arweave为区块链运行机制提出的另外一种思路-链下计算。</p><h1 id="h-arweave" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Arweave不只于存储</h1><h2 id="h-smartweave" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">SmartWeave</h2><p>SmartWeave是Arweave的智能合约平台，其机制是在链下执行合约调用，然后将状态存储在Arweave中。是不是似曾相识,我们之前介绍过Layer2的Rollup也是这种机制。SmartWeave规定合约的执行是由调用者完成。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c0f8753b88455c12e3491bca4c7285ad6dc6082b7236d83e46ad898de55af33b.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>在之前Layer2一文中我们了解过，受限于不可能三角，为了提升L1的TPS将执行过程放在单独一层L2，而L1只进行状态的存储，但就L2的方案需要解决的1个核心问题就是如何证明L2向L1发布的状态是正确的，L2的实现方案之前一文已经讨论过，这里不再赘述。我们看到SmartWeave也是将Arweave只作为存储，而交易执行(状态转移过程)放在链下。那在SmartWeave中是否需要证明发布到Arweave的状态的正确性呢？</p><p>SmartWeave并不需要发布者证明其发布的状态正确性，而是采用惰性评估，即每次合约调用者在调用合约的时候，需要将合约相关的交易全部执行一次进行状态转移。相当于将状态的正确性交由下一次合约的调用者来验证。这种方式极大地释放了链上计算量，使得SmartWeave TPS (吞吐量)大幅优于其他L1链。</p><p>理论美好，但实际落地遇到了很多问题，目前看SmartWeave并未对发布错误状态的用户做任何惩罚机制。这势必会导致大量用户可以发起很多无效调用来增加后续每次合约调用者的耗时，也就是DDOS攻击。</p><p>就目前而言，Arweave还是以永久存储为主要作用，而SmartWeave这种机制还有不少问题有待解决。</p><h1 id="h-" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h1><p>本文介绍了去中心化存储的另外1种解决方案Arweave的实现，通过钱包列表和哈希列表实现了每个节点只存储部分区块，通过POA和blockweave结构实现了数据冗余，从而达成永久存储的目标。文中还对比了filecoin和Arweave的优劣，目前来看Arweave落地的更好一些。最后我们通过Arweave的smartweave引出了一种基于存储的共识范式。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/7a13d2f25c66c0a4bdd7d986f6450b66b9d3b7eae4e218f6a72cb94ab0dfc77e.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[智能合约安全性研究 ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/GdQwmrgu1imuN8mZUxeD</link>
            <guid>GdQwmrgu1imuN8mZUxeD</guid>
            <pubDate>Sun, 22 May 2022 01:25:39 GMT</pubDate>
            <description><![CDATA[本文向大家介绍了常见的合约漏洞，自动化监测合约漏洞的工具以及链上漏洞的应对方案。这些内容不仅仅对大家更好保护资产有帮助而且也有助于更加深入理解智能合约的运作流程。 由于合约和资产的强关联以及合约不可修改的特性，合约漏洞会对项目发起者和参与项目的用户带来财产损失以及信任危机。所以我们要尽可能地避免发生合约被攻击的情况。本文主要分为3个部分:介绍常见的合约漏洞，在开发阶段避免一些可能被攻击的写法。介绍一些自动化合约漏洞检测工具，在测试阶段更加快捷地发现漏洞。介绍合约上链后通过合约暂停，合约监控以及合约升级的方式来避免合约漏洞带来的损失。合约漏洞数值溢出较为典型的是if(a - b > 0)的判断，其实无符号整数溢出在C语言也存在，对于无符号整数而言永远无法通过相减得到小于0的数，(0-1)会获得取值范围内最大的数，就像循环一样。pragma solidity ^0.6.0; contract Token { mapping(address => uint) balances; uint public totalSupply; constructor(uint _initialSupp...]]></description>
            <content:encoded><![CDATA[<p>本文向大家介绍了常见的合约漏洞，自动化监测合约漏洞的工具以及链上漏洞的应对方案。这些内容不仅仅对大家更好保护资产有帮助而且也有助于更加深入理解智能合约的运作流程。</p><p>由于合约和资产的强关联以及合约不可修改的特性，合约漏洞会对项目发起者和参与项目的用户带来财产损失以及信任危机。所以我们要尽可能地避免发生合约被攻击的情况。本文主要分为3个部分:</p><ol><li><p>介绍常见的合约漏洞，在开发阶段避免一些可能被攻击的写法。</p></li><li><p>介绍一些自动化合约漏洞检测工具，在测试阶段更加快捷地发现漏洞。</p></li><li><p>介绍合约上链后通过合约暂停，合约监控以及合约升级的方式来避免合约漏洞带来的损失。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约漏洞</h2><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">数值溢出</h2><p>较为典型的是if(a - b &gt; 0)的判断，其实无符号整数溢出在C语言也存在，对于无符号整数而言永远无法通过相减得到小于0的数，(0-1)会获得取值范围内最大的数，就像循环一样。</p><pre data-type="codeBlock" text="pragma solidity ^0.6.0;

contract Token {
  mapping(address =&gt; uint) balances;
  uint public totalSupply;
  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }
  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value &gt;= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }
  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.6.0;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Token</span> </span>{
  <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">uint</span>) balances;
  <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> totalSupply;
  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _initialSupply</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> totalSupply <span class="hljs-operator">=</span> _initialSupply;
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _to, <span class="hljs-keyword">uint</span> _value</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
    <span class="hljs-built_in">require</span>(balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">-</span> _value <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
    balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">-</span><span class="hljs-operator">=</span> _value;
    balances[_to] <span class="hljs-operator">+</span><span class="hljs-operator">=</span> _value;
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _owner</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> balance</span>) </span>{
    <span class="hljs-keyword">return</span> balances[_owner];
  }
}
</code></pre><p>上面的合约大家可以尝试调用transfer函数传入1个比自己余额大的值，是否可以完美通过。</p><p>避免此类问题，有2种方案，其原理都是在计算前后进行判定:</p><ol><li><p>使用openzeppelin提供的SafeMath</p></li></ol><pre data-type="codeBlock" text="import &apos;@openzeppelin/contracts/math/SafeMath.sol&apos;;
using SafeMath for uint256;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'@openzeppelin/contracts/math/SafeMath.sol'</span>;
<span class="hljs-keyword">using</span> <span class="hljs-title">SafeMath</span> <span class="hljs-title"><span class="hljs-keyword">for</span></span> <span class="hljs-title"><span class="hljs-keyword">uint256</span></span>;
</code></pre><ol start="2"><li><p>solidity版本使用8.0以上</p></li></ol><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">私有变量可读取</h2><p>private作用只是无法直接让其他合约调用，但并不代表通过private存储的值无法被获取，从计算机的基础理论也容易想到，值都是存在内存或磁盘中的，只要我们了解存储规则，private是没有任何作用的。这一点其他编程语言中也是一致的，变量的权限修饰符从来都是为了代码设计，而不是加密。 如同下面的合约</p><pre data-type="codeBlock" text="pragma solidity ^0.6.0;
contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.6.0;</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Vault</span> </span>{
  <span class="hljs-keyword">bool</span> <span class="hljs-keyword">public</span> locked;
  <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">private</span> password;

  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _password</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    locked <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    password <span class="hljs-operator">=</span> _password;
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">unlock</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _password</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">if</span> (password <span class="hljs-operator">=</span><span class="hljs-operator">=</span> _password) {
      locked <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
    }
  }
}
</code></pre><p>之前文章<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s/5x1AZ_W_gxCcQ4Vy9TsNnA">智能合约升级详解</a>时介绍过合约存储是按照槽位存储的，我们只需要指定存储槽位即可读取到相应变量的值:</p><pre data-type="codeBlock" text="const res = await provider.getStorageAt(&quot;合约地址&quot;, 1);
 console.log(`res is: ${res}`);
"><code>const res <span class="hljs-operator">=</span> await provider.getStorageAt(<span class="hljs-string">"合约地址"</span>, <span class="hljs-number">1</span>);
 console.log(`res <span class="hljs-keyword">is</span>: ${res}`);
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">链上随机数不随机</h2><p>链上没有随机数可言，因为链上代码都是开源的，链上制造随机数的操作，都可以使用另外1个合约执行相同的操作得到。不要希望使用block相关变量得到随机数，模拟者放到同1个区块内执行即可。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">权限设置</h2><p>权限设置更多的是合约设计的考量，我们这里主要介绍一个经典的问题。</p><h3 id="h-msgsender-txorigin" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">使用 msg.sender 而不是 tx.origin 做权限校验</h3><p>tx.origin是指整个交易链路的首位发起者，而msg.sender才是真正调用合约者，如果是外部账户-&gt;合约的简单调用时,其获取的地址是一致的，除此之外这2种方式获取的地址均不相同。 如下合约使用tx.origin进行校验权限</p><pre data-type="codeBlock" text="contract TxUserWallet {
    address owner;
    constructor() {
        owner = msg.sender;
    }
    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">TxUserWallet</span> </span>{
    <span class="hljs-keyword">address</span> owner;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        owner <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferTo</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> dest, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner);
        dest.<span class="hljs-built_in">transfer</span>(amount);
    }
}
</code></pre><p>当被攻击合约向攻击合约转账时，攻击合约fallback函数被调用，此时被攻击合约通过tx.origin获得的是整个交易的最初发起者也就是被攻击合约的owner，校验通过。</p><pre data-type="codeBlock" text="interface TxUserWallet {
    function transferTo(address dest, uint amount) public;
}

contract TxAttackWallet {
    address payable owner;

    constructor() {
        owner = payable(msg.sender);
    }

    function() public {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">TxUserWallet</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferTo</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> dest, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span></span>;
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">TxAttackWallet</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">payable</span> owner;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        owner <span class="hljs-operator">=</span> <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        TxUserWallet(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).transferTo(owner, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">balance</span>);
    }
}
</code></pre><p>对于权限控制建议使用openzeppelin contracts包下提供的工具合约。一般使用onlyOwner即可，如果需要较为复杂的角色控制，可以使用AccessControl。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">拒绝访问攻击</h2><p>拒绝访问一般出现在合约循环对外转账的场景下，如果对外转账的对象是1个合约账号，其在fallback函数中抛出错误，那么合约循环将无法继续。所以通常情况下要避免循环对外转账，尽量使用满足条件的账户自提的方式，这样可以避免恶意账户埋雷导致其他账户资产受到影响。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">重入攻击</h2><p>重入比较常见的是利用被攻击合约向攻击合约转账时会调用攻击合约的fallback函数，攻击合约一般会在fallback函数中再次调用被攻击合约以达到重新执行函数的目的。</p><pre data-type="codeBlock" text="contract Fund {
    mapping(address =&gt; uint) shares;
    function withdraw() public {
        if (payable(msg.sender).call.value(shares[msg.sender])())
            shares[msg.sender] = 0;
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Fund</span> </span>{
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">uint</span>) shares;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">call</span>.<span class="hljs-built_in">value</span>(shares[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>])())
            shares[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    }
}
</code></pre><p>攻击者只需要在合约的fallback函数中继续调用该函数即可耗尽合约资金。</p><pre data-type="codeBlock" text="interface Fund {
    function withdraw() external;
}
contract Attack {
    address payable owner;
    constructor() {
        owner = payable(msg.sender);
    }
    function attck(address addr) public {
        Fund f = Fund(addr);
        f.withdraw();
    }
    fallback() external payable{
        Fund f = Fund(msg.sender);
        f.withdraw();
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Fund</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
}
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Attack</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">payable</span> owner;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        owner <span class="hljs-operator">=</span> <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attck</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        Fund f <span class="hljs-operator">=</span> Fund(addr);
        f.withdraw();
    }
    <span class="hljs-function"><span class="hljs-keyword">fallback</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span></span>{
        Fund f <span class="hljs-operator">=</span> Fund(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
        f.withdraw();
    }
}
</code></pre><p>为了避免重入，我们需要践行“检查-生效-交互”的设计模式，检查-生效-交互是指，应该先进行条件满足的判断，然后修改合约中的状态，最后再与外部账户进行交互，这样如果外部合约发动重入攻击也是在状态修改之后，重入时会被条件判断拦截。上面的合约应做如下修改:</p><pre data-type="codeBlock" text="contract Fund {
    mapping(address =&gt; uint) shares;
    function withdraw() public {
        var share = shares[msg.sender];
        if(share &gt; 0) {
          shares[msg.sender] = 0;
          payable(msg.sender).transfer(share);
        }
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Fund</span> </span>{
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">uint</span>) shares;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">var</span> share <span class="hljs-operator">=</span> shares[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>];
        <span class="hljs-keyword">if</span>(share <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
          shares[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
          <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">transfer</span>(share);
        }
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">重入锁</h3><p>此外openzeppelin还为我们提供了重入锁来帮助我们解决重入问题。重入锁的实现如下所示,也是使用了“检查-生效-交互”模式:</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
abstract contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;
    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        //如果状态是初始状态则进入函数
        require(_status != _ENTERED, &quot;ReentrancyGuard: reentrant call&quot;);
        //将状态置为访问中
        _status = _ENTERED;
        //执行函数
        _;
        //将状态置为初始状态
        _status = _NOT_ENTERED;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ReentrancyGuard</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _NOT_ENTERED <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _ENTERED <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> _status;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        _status <span class="hljs-operator">=</span> _NOT_ENTERED;
    }

    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">nonReentrant</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">//如果状态是初始状态则进入函数</span>
        <span class="hljs-built_in">require</span>(_status <span class="hljs-operator">!</span><span class="hljs-operator">=</span> _ENTERED, <span class="hljs-string">"ReentrancyGuard: reentrant call"</span>);
        <span class="hljs-comment">//将状态置为访问中</span>
        _status <span class="hljs-operator">=</span> _ENTERED;
        <span class="hljs-comment">//执行函数</span>
        <span class="hljs-keyword">_</span>;
        <span class="hljs-comment">//将状态置为初始状态</span>
        _status <span class="hljs-operator">=</span> _NOT_ENTERED;
    }
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">条件错误锁死</h2><p>在一些关键的操作，我们对于条件判断的一定要仔细考虑，编写足够多的测试用例进行测试。</p><h3 id="h-akutar3400yuan" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Akutar合约条件错误导致3400万美元资金永久被锁</h3><p>在这个例子中取回合约中的资金claimProjectFunds函数有3个条件。其中refundProgress &gt;= totalBids条件，大家可以观察refundProgress和totalBids赋值的地方。应该不难发现只要有1次调用_bid函数时amount&gt;1，则refundProgress &gt;= totalBids的结果不可能被满足，这也直接导致该合约内3400万美元被锁死。同时该合约在processRefunds函数存在上文中提到的循环对外转账的操作，非常容易引入拒绝访问攻击。</p><pre data-type="codeBlock" text="function claimProjectFunds() external onlyOwner {
        require(block.timestamp &gt; expiresAt, &quot;Auction still in progress&quot;);
        require(refundProgress &gt;= totalBids, &quot;Refunds not yet processed&quot;);
        require(akuNFTs.airdropProgress() &gt;= totalBids, &quot;Airdrop not complete&quot;);

        (bool sent, ) = project.call{value: address(this).balance}(&quot;&quot;);
        require(sent, &quot;Failed to withdraw&quot;);        
  }

  function processRefunds() external {
      require(block.timestamp &gt; expiresAt, &quot;Auction still in progress&quot;);
      uint256 _refundProgress = refundProgress;
      uint256 _bidIndex = bidIndex;
      require(_refundProgress &amp;lt; _bidIndex, &quot;Refunds already processed&quot;);

      uint256 gasUsed;
      uint256 gasLeft = gasleft();
      uint256 price = getPrice();

      for (uint256 i=_refundProgress; gasUsed &amp;lt; 5000000 &amp;&amp; i &amp;lt; _bidIndex; i++) {
          bids memory bidData = allBids[i];
          if (bidData.finalProcess == 0) {
            uint256 refund = (bidData.price - price) * bidData.bidsPlaced;
            uint256 passes = mintPassOwner[bidData.bidder];
            if (passes &gt; 0) {
                refund += mintPassDiscount * (bidData.bidsPlaced &amp;lt; passes ? bidData.bidsPlaced : passes);
            }
            allBids[i].finalProcess = 1;
            if (refund &gt; 0) {
                (bool sent, ) = bidData.bidder.call{value: refund}(&quot;&quot;);
                require(sent, &quot;Failed to refund bidder&quot;);
            }
          }

          gasUsed += gasLeft - gasleft();
          gasLeft = gasleft();
          _refundProgress++;
      }

      refundProgress = _refundProgress;
    }

    function _bid(uint8 amount, uint256 value) internal {
        require(block.timestamp &gt; startAt, &quot;Auction not started yet&quot;);
        require(block.timestamp &amp;lt; expiresAt, &quot;Auction expired&quot;);
        uint80 price = getPrice();
        uint256 totalPrice = price * amount;
        if (value &amp;lt; totalPrice) {
            revert(&quot;Bid not high enough&quot;);
        }

        uint256 myBidIndex = personalBids[msg.sender];
        bids memory myBids;
        uint256 refund;

        if (myBidIndex &gt; 0) {
            myBids = allBids[myBidIndex];
            refund = myBids.bidsPlaced * (myBids.price - price);
        }
        uint256 _totalBids = totalBids + amount;
        myBids.bidsPlaced += amount;

        if (myBids.bidsPlaced &gt; maxBids) {
            revert(&quot;Bidding limits exceeded&quot;);
        }

        if(_totalBids &gt; totalForAuction) {
            revert(&quot;Auction Full&quot;);
        } else if (_totalBids == totalForAuction) {
            expiresAt = block.timestamp; //Auction filled
        }

        myBids.price = price;

        if (myBidIndex &gt; 0) {
            allBids[myBidIndex] = myBids;
        } else {
            myBids.bidder = msg.sender;
            personalBids[msg.sender] = bidIndex;
            allBids[bidIndex] = myBids;
            bidIndex++;
        }

        totalBids = _totalBids;
        totalBidValue += totalPrice;

        refund += value - totalPrice;
        if (refund &gt; 0) {
            (bool sent, ) = msg.sender.call{value: refund}(&quot;&quot;);
            require(sent, &quot;Failed to refund bidder&quot;);
        }
    }
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimProjectFunds</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">></span> expiresAt, <span class="hljs-string">"Auction still in progress"</span>);
        <span class="hljs-built_in">require</span>(refundProgress <span class="hljs-operator">></span><span class="hljs-operator">=</span> totalBids, <span class="hljs-string">"Refunds not yet processed"</span>);
        <span class="hljs-built_in">require</span>(akuNFTs.airdropProgress() <span class="hljs-operator">></span><span class="hljs-operator">=</span> totalBids, <span class="hljs-string">"Airdrop not complete"</span>);

        (<span class="hljs-keyword">bool</span> sent, ) <span class="hljs-operator">=</span> project.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>}(<span class="hljs-string">""</span>);
        <span class="hljs-built_in">require</span>(sent, <span class="hljs-string">"Failed to withdraw"</span>);        
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processRefunds</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
      <span class="hljs-built_in">require</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">></span> expiresAt, <span class="hljs-string">"Auction still in progress"</span>);
      <span class="hljs-keyword">uint256</span> _refundProgress <span class="hljs-operator">=</span> refundProgress;
      <span class="hljs-keyword">uint256</span> _bidIndex <span class="hljs-operator">=</span> bidIndex;
      <span class="hljs-built_in">require</span>(_refundProgress <span class="hljs-operator">&#x26;</span>lt; _bidIndex, <span class="hljs-string">"Refunds already processed"</span>);

      <span class="hljs-keyword">uint256</span> gasUsed;
      <span class="hljs-keyword">uint256</span> gasLeft <span class="hljs-operator">=</span> <span class="hljs-built_in">gasleft</span>();
      <span class="hljs-keyword">uint256</span> price <span class="hljs-operator">=</span> getPrice();

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i<span class="hljs-operator">=</span>_refundProgress; gasUsed <span class="hljs-operator">&#x26;</span>lt; <span class="hljs-number">5000000</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> i <span class="hljs-operator">&#x26;</span>lt; _bidIndex; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
          bids <span class="hljs-keyword">memory</span> bidData <span class="hljs-operator">=</span> allBids[i];
          <span class="hljs-keyword">if</span> (bidData.finalProcess <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">uint256</span> refund <span class="hljs-operator">=</span> (bidData.price <span class="hljs-operator">-</span> price) <span class="hljs-operator">*</span> bidData.bidsPlaced;
            <span class="hljs-keyword">uint256</span> passes <span class="hljs-operator">=</span> mintPassOwner[bidData.bidder];
            <span class="hljs-keyword">if</span> (passes <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
                refund <span class="hljs-operator">+</span><span class="hljs-operator">=</span> mintPassDiscount <span class="hljs-operator">*</span> (bidData.bidsPlaced <span class="hljs-operator">&#x26;</span>lt; passes ? bidData.bidsPlaced : passes);
            }
            allBids[i].finalProcess <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
            <span class="hljs-keyword">if</span> (refund <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
                (<span class="hljs-keyword">bool</span> sent, ) <span class="hljs-operator">=</span> bidData.bidder.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: refund}(<span class="hljs-string">""</span>);
                <span class="hljs-built_in">require</span>(sent, <span class="hljs-string">"Failed to refund bidder"</span>);
            }
          }

          gasUsed <span class="hljs-operator">+</span><span class="hljs-operator">=</span> gasLeft <span class="hljs-operator">-</span> <span class="hljs-built_in">gasleft</span>();
          gasLeft <span class="hljs-operator">=</span> <span class="hljs-built_in">gasleft</span>();
          _refundProgress<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
      }

      refundProgress <span class="hljs-operator">=</span> _refundProgress;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_bid</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span> amount, <span class="hljs-keyword">uint256</span> value</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">></span> startAt, <span class="hljs-string">"Auction not started yet"</span>);
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&#x26;</span>lt; expiresAt, <span class="hljs-string">"Auction expired"</span>);
        <span class="hljs-keyword">uint80</span> price <span class="hljs-operator">=</span> getPrice();
        <span class="hljs-keyword">uint256</span> totalPrice <span class="hljs-operator">=</span> price <span class="hljs-operator">*</span> amount;
        <span class="hljs-keyword">if</span> (value <span class="hljs-operator">&#x26;</span>lt; totalPrice) {
            <span class="hljs-keyword">revert</span>(<span class="hljs-string">"Bid not high enough"</span>);
        }

        <span class="hljs-keyword">uint256</span> myBidIndex <span class="hljs-operator">=</span> personalBids[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>];
        bids <span class="hljs-keyword">memory</span> myBids;
        <span class="hljs-keyword">uint256</span> refund;

        <span class="hljs-keyword">if</span> (myBidIndex <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
            myBids <span class="hljs-operator">=</span> allBids[myBidIndex];
            refund <span class="hljs-operator">=</span> myBids.bidsPlaced <span class="hljs-operator">*</span> (myBids.price <span class="hljs-operator">-</span> price);
        }
        <span class="hljs-keyword">uint256</span> _totalBids <span class="hljs-operator">=</span> totalBids <span class="hljs-operator">+</span> amount;
        myBids.bidsPlaced <span class="hljs-operator">+</span><span class="hljs-operator">=</span> amount;

        <span class="hljs-keyword">if</span> (myBids.bidsPlaced <span class="hljs-operator">></span> maxBids) {
            <span class="hljs-keyword">revert</span>(<span class="hljs-string">"Bidding limits exceeded"</span>);
        }

        <span class="hljs-keyword">if</span>(_totalBids <span class="hljs-operator">></span> totalForAuction) {
            <span class="hljs-keyword">revert</span>(<span class="hljs-string">"Auction Full"</span>);
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (_totalBids <span class="hljs-operator">=</span><span class="hljs-operator">=</span> totalForAuction) {
            expiresAt <span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>; <span class="hljs-comment">//Auction filled</span>
        }

        myBids.price <span class="hljs-operator">=</span> price;

        <span class="hljs-keyword">if</span> (myBidIndex <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
            allBids[myBidIndex] <span class="hljs-operator">=</span> myBids;
        } <span class="hljs-keyword">else</span> {
            myBids.bidder <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
            personalBids[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> bidIndex;
            allBids[bidIndex] <span class="hljs-operator">=</span> myBids;
            bidIndex<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
        }

        totalBids <span class="hljs-operator">=</span> _totalBids;
        totalBidValue <span class="hljs-operator">+</span><span class="hljs-operator">=</span> totalPrice;

        refund <span class="hljs-operator">+</span><span class="hljs-operator">=</span> value <span class="hljs-operator">-</span> totalPrice;
        <span class="hljs-keyword">if</span> (refund <span class="hljs-operator">></span> <span class="hljs-number">0</span>) {
            (<span class="hljs-keyword">bool</span> sent, ) <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: refund}(<span class="hljs-string">""</span>);
            <span class="hljs-built_in">require</span>(sent, <span class="hljs-string">"Failed to refund bidder"</span>);
        }
    }
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">签名重放</h2><h3 id="h-nbaxnft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">NBAXNFT签名重放白名单被耗尽</h3><p>在这个例子中NBAXNFT合约本意是想为白名单用户提供mint，但由于verify函数校验白名单时没有校验msg.sender的地址和参数info中的地址是否相同，任何人在传入info时都可以使用已经通过验证的签名信息，合约代码中真正mint发放的时候又使用msg.sender。这里会导致2个问题，第1个问题是非白名单用户也可以通过重放签名进行Mint，第2个问题是白名单用户可以重复mint，因为对于1个地址签名是确定的。</p><pre data-type="codeBlock" text="function mint_approved(
        vData memory info,
        uint256 number_of_items_requested,
        uint16 _batchNumber
    ) external {
        require(batchNumber == _batchNumber, &quot;!batch&quot;);
        address from = msg.sender;
        require(verify(info), &quot;Unauthorised access secret&quot;);
        _discountedClaimedPerWallet[msg.sender] += 1;
        require(
            _discountedClaimedPerWallet[msg.sender] &amp;lt;= 1,
            &quot;Number exceeds max discounted per address&quot;
        );
        presold[from] = 1;
        _mintCards(number_of_items_requested, from);
        emit batchWhitelistMint(_batchNumber, msg.sender);
    }
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint_approved</span>(<span class="hljs-params">
        vData <span class="hljs-keyword">memory</span> info,
        <span class="hljs-keyword">uint256</span> number_of_items_requested,
        <span class="hljs-keyword">uint16</span> _batchNumber
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-built_in">require</span>(batchNumber <span class="hljs-operator">=</span><span class="hljs-operator">=</span> _batchNumber, <span class="hljs-string">"!batch"</span>);
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        <span class="hljs-built_in">require</span>(verify(info), <span class="hljs-string">"Unauthorised access secret"</span>);
        _discountedClaimedPerWallet[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        <span class="hljs-built_in">require</span>(
            _discountedClaimedPerWallet[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">=</span> <span class="hljs-number">1</span>,
            <span class="hljs-string">"Number exceeds max discounted per address"</span>
        );
        presold[<span class="hljs-keyword">from</span>] <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        _mintCards(number_of_items_requested, <span class="hljs-keyword">from</span>);
        <span class="hljs-keyword">emit</span> batchWhitelistMint(_batchNumber, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
    }
</code></pre><pre data-type="codeBlock" text="function verify(vData memory info) public view returns (bool) {
        require(info.from != address(0), &quot;INVALID_SIGNER&quot;);
        bytes memory cat =
            abi.encode(
                info.from,
                info.start,
                info.end,
                info.eth_price,
                info.dust_price,
                info.max_mint,
                info.mint_free
            );
        // console.log(&quot;data--&gt;&quot;);
        // console.logBytes(cat);
        bytes32 hash = keccak256(cat);
        // console.log(&quot;hash -&gt;&quot;);
        //    console.logBytes32(hash);
        require(info.signature.length == 65, &quot;Invalid signature length&quot;);
        bytes32 sigR;
        bytes32 sigS;
        uint8 sigV;
        bytes memory signature = info.signature;
        // ecrecover takes the signature parameters, and the only way to get them
        // currently is to use assembly.
        assembly {
            sigR := mload(add(signature, 0x20))
            sigS := mload(add(signature, 0x40))
            sigV := byte(0, mload(add(signature, 0x60)))
        }

        bytes32 data =
            keccak256(
                abi.encodePacked(&quot;\x19Ethereum Signed Message:\n32&quot;, hash)
            );
        address recovered = ecrecover(data, sigV, sigR, sigS);
        return signer == recovered;
    }
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verify</span>(<span class="hljs-params">vData <span class="hljs-keyword">memory</span> info</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
        <span class="hljs-built_in">require</span>(info.from <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">"INVALID_SIGNER"</span>);
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> cat <span class="hljs-operator">=</span>
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                info.from,
                info.start,
                info.end,
                info.eth_price,
                info.dust_price,
                info.max_mint,
                info.mint_free
            );
        <span class="hljs-comment">// console.log("data-->");</span>
        <span class="hljs-comment">// console.logBytes(cat);</span>
        <span class="hljs-keyword">bytes32</span> hash <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(cat);
        <span class="hljs-comment">// console.log("hash ->");</span>
        <span class="hljs-comment">//    console.logBytes32(hash);</span>
        <span class="hljs-built_in">require</span>(info.signature.<span class="hljs-built_in">length</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">65</span>, <span class="hljs-string">"Invalid signature length"</span>);
        <span class="hljs-keyword">bytes32</span> sigR;
        <span class="hljs-keyword">bytes32</span> sigS;
        <span class="hljs-keyword">uint8</span> sigV;
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature <span class="hljs-operator">=</span> info.signature;
        <span class="hljs-comment">// ecrecover takes the signature parameters, and the only way to get them</span>
        <span class="hljs-comment">// currently is to use assembly.</span>
        <span class="hljs-keyword">assembly</span> {
            sigR <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">0x20</span>))
            sigS <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">0x40</span>))
            sigV <span class="hljs-operator">:=</span> <span class="hljs-built_in">byte</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">0x60</span>)))
        }

        <span class="hljs-keyword">bytes32</span> data <span class="hljs-operator">=</span>
            <span class="hljs-built_in">keccak256</span>(
                <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(<span class="hljs-string">"\x19Ethereum Signed Message:\n32"</span>, hash)
            );
        <span class="hljs-keyword">address</span> recovered <span class="hljs-operator">=</span> <span class="hljs-built_in">ecrecover</span>(data, sigV, sigR, sigS);
        <span class="hljs-keyword">return</span> signer <span class="hljs-operator">=</span><span class="hljs-operator">=</span> recovered;
    }
</code></pre><ul><li><p>第一个问题我们可以通过增加msg.sender == info.from来避免非白名单用户mint，但这样会限制委托mint，我们其实只需要保证校验的地址和最终mint的地址一致即可。</p></li><li><p>第二个问题需要通过引入nonce来解决，在进行签名时加入nonce值, 然后在合约中记录每个地址使用过的nonce,如果已经使用则交易无效，下面是EIP-712避免签名重放攻击的示例</p></li></ul><pre data-type="codeBlock" text="function transferWithSignature(
    address recipient, 
    uint256 amount, 
    address sender, 
    bytes32 nonce, 
    bytes memory signature
  ) public returns (bool) {

    // Hash of structured data as defined according to the TYPEHASH
    bytes32 hashStruct = keccak256(abi.encode(
      TRANSFER_TYPEHASH,
      sender,
      recipient,
      amount,
      nonce
    ));

    // Signed hash as defined by EIP-712
    bytes32 hashed = keccak256(abi.encodePacked(
      &quot;\x19\x01&quot;,
      domainSeparator,
      hashStruct
    ));

    address recovered = ECDSA.recover(hashed, signature);
    require(recovered == sender, &quot;Signature mismatch&quot;);

    require(!nonces[sender][nonce], &quot;Repeated nonce&quot;);
    nonces[sender][nonce] = true;

    _transfer(sender, recipient, amount);
  }
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferWithSignature</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> recipient, 
    <span class="hljs-keyword">uint256</span> amount, 
    <span class="hljs-keyword">address</span> sender, 
    <span class="hljs-keyword">bytes32</span> nonce, 
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature
  </span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{

    <span class="hljs-comment">// Hash of structured data as defined according to the TYPEHASH</span>
    <span class="hljs-keyword">bytes32</span> hashStruct <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
      TRANSFER_TYPEHASH,
      sender,
      recipient,
      amount,
      nonce
    ));

    <span class="hljs-comment">// Signed hash as defined by EIP-712</span>
    <span class="hljs-keyword">bytes32</span> hashed <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
      <span class="hljs-string">"\x19\x01"</span>,
      domainSeparator,
      hashStruct
    ));

    <span class="hljs-keyword">address</span> recovered <span class="hljs-operator">=</span> ECDSA.recover(hashed, signature);
    <span class="hljs-built_in">require</span>(recovered <span class="hljs-operator">=</span><span class="hljs-operator">=</span> sender, <span class="hljs-string">"Signature mismatch"</span>);

    <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>nonces[sender][nonce], <span class="hljs-string">"Repeated nonce"</span>);
    nonces[sender][nonce] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

    _transfer(sender, recipient, amount);
  }
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约漏洞自动化检测工具</h2><p>编写完合约代码后我们需要经过严格的测试，如果较大的项目需要进行专业的合约审计。下面介绍2款自动化漏洞检测工具能够对我们的人工测试进行有效补充。</p><h2 id="h-slither" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">slither</h2><p>slither是Solidity代码的静态分析工具，可以在不执行代码的情况下对代码进行静态分析。 分析命令:</p><pre data-type="codeBlock" text="slither 合约路径
"><code></code></pre><p>官方链接: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/crytic/slither">https://github.com/crytic/slither</a></p><h2 id="h-echidna" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">echidna</h2><p>echidna为我们提供状态分析，我们在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s/4aSvNCpluBt4BezVPQnm8A">聊一聊智能合约</a>介绍过可以将智能合约执行的过程理解为状态机状态变化的过程。echidna帮助我们分析状态的可达性。如果某些状态无论如何执行都符合我们预期则测试通过，如果有些执行路径不满足预期，则会输出不满足预期情况下的调用情况。</p><p>echidna要求我们编写状态检测函数</p><pre data-type="codeBlock" text="function echidna_check_balance() public returns (bool) {
    return(balance &gt;= 20);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">echidna_check_balance</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
    <span class="hljs-keyword">return</span>(balance <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">20</span>);
}
</code></pre><p>执行合约检查</p><pre data-type="codeBlock" text="chidna-test 合约地址
"><code>chidna<span class="hljs-operator">-</span>test 合约地址
</code></pre><p>官方链接: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/crytic/echidna">https://github.com/crytic/echidna</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约监控修复方案</h2><h2 id="h-and" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约可暂停&amp;链上合约监控</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约可暂停</h3><p>当我们发现线上合约存在漏洞时，我们需要第一时间暂停合约的正常运行来避免合约被攻击即只保留个别函数可以被正常调用，其余函数均无法被调用，openzeppelin为我们提供了Pausable工具合约来帮助我们有效实现这一目标。如下所示，当发现合约存在问题时我们调用_pause将合约暂停</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;

import &quot;../utils/Context.sol&quot;;
abstract contract Pausable is Context {
    event Paused(address account);
    event Unpaused(address account);
    bool private _paused;
    constructor() {
        _paused = false;
    }
    function paused() public view virtual returns (bool) {
        return _paused;
    }
    modifier whenNotPaused() {
        require(!paused(), &quot;Pausable: paused&quot;);
        _;
    }
    modifier whenPaused() {
        require(paused(), &quot;Pausable: not paused&quot;);
        _;
    }
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"../utils/Context.sol"</span>;
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Pausable</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Context</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Paused</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Unpaused</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>)</span>;
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">private</span> _paused;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        _paused <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">paused</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
        <span class="hljs-keyword">return</span> _paused;
    }
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">whenNotPaused</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>paused(), <span class="hljs-string">"Pausable: paused"</span>);
        <span class="hljs-keyword">_</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">whenPaused</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(paused(), <span class="hljs-string">"Pausable: not paused"</span>);
        <span class="hljs-keyword">_</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_pause</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title">whenNotPaused</span> </span>{
        _paused <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">emit</span> Paused(_msgSender());
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_unpause</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title">whenPaused</span> </span>{
        _paused <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
        <span class="hljs-keyword">emit</span> Unpaused(_msgSender());
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">链上合约监控</h3><p>上面我们介绍了如何在发现漏洞时暂停合约，如何及时发现漏洞也有较多方案，我们在之前<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s/3Z2g946kwnwYJc17Tq1guw">使用TheGraph实现Dapp的万倍速度优化</a>介绍了TheGraph的使用。我们对链上一些异常的监控也可以使用TheGraph,在发现链上数据有异常时及时报警。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约可升级</h2><p>当合约暂停后，我们期望最好的结果肯定是进行修复后合约能够继续正常运转。合约虽然上链之后代码不可更改，但我们可以通过代理合约的方式实现合约的动态升级，在之前<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s/5x1AZ_W_gxCcQ4Vy9TsNnA">智能合约升级详解</a>中已经进行了详细介绍。通过合约升级可以将我们发现存在风险的链上合约进行升级修复。但这里再次强调一次，使用合约升级时需要配合升级治理，否则项目参与者对合约的信任度会降低。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本文向大家介绍了常见的合约漏洞，自动化监测合约漏洞的工具以及链上漏洞的应对方案。这些内容不仅仅对大家更好保护资产有帮助而且也有助于更加深入理解智能合约的运作流程。合约漏洞是web3安全的一个重要组成，但不是唯一因素。此外还包含链下钓鱼、跨链安全(跨链桥合约的安全)等等。希望大家能写出更加安全的合约代码。</p><p>相关链接:</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/">https://github.com/OpenZeppelin/</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/crytic/echidna">https://github.com/crytic/echidna</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/crytic/slither">https://github.com/crytic/slither</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethernaut.openzeppelin.com/">https://ethernaut.openzeppelin.com/</a></p></li></ul>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/d4452d8c21752892b6d72de01c6c517c8346c44dc0ab3d11d05fc777deb0ac6b.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[使用TheGraph实现Dapp的万倍速度优化 ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/thegraph-dapp</link>
            <guid>PcLtfnDYHqBjePCSxBMY</guid>
            <pubDate>Sat, 14 May 2022 04:34:53 GMT</pubDate>
            <description><![CDATA[本文我们从NFT列表展示速度慢的现象开始分析，发现前端展示流程不合理的地方，通过引入TheGraph将NFT列表展示耗时降低了1万多倍，实际中还有很多类似的应用。极慢的前端体验6407.jpeg如图所示，我们需要不断循环请求获取tokenId对应的tokenURI，然后通过tokenURI请求获取metadata来解析数据，每次循环都是2次http通信 按照较低时延http通信耗时300ms，每次循环将消耗600ms, 而且这个数字将随着NFT数量增加线性增加，我们的页面耗时5、6s是因为我们的NFT只mint了10个左右，正常上线的NFT一般会有接近1万个toknId，那么整体耗时将达到 100分钟，这样的速度没有任何用户可以容忍。所以我们一定需要其他方案来优化这一流程。6408.jpeg理想状态下前端页面只需要1次请求获取所需要的数据即可，而不是进行多次请求。这就需要1个链下的后端服务帮我们去做类似的逻辑并存储数据，待前端页面需要展示时请求这个后端服务中存储的数据，而TheGraph就是这样的一种服务，与我们自行搭建的后端服务不同，TheGraph是去中心化的，这极大的避免了...]]></description>
            <content:encoded><![CDATA[<p>本文我们从NFT列表展示速度慢的现象开始分析，发现前端展示流程不合理的地方，通过引入TheGraph将NFT列表展示耗时降低了1万多倍，实际中还有很多类似的应用。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">极慢的前端体验</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d44773ac818fbe758c8b85e45f7775140ff81e83f8ed7b707db9b6569f9d3d1b.jpg" alt="6407.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6407.jpeg</figcaption></figure><p>如图所示，我们需要不断循环请求获取tokenId对应的tokenURI，然后通过tokenURI请求获取metadata来解析数据，每次循环都是2次http通信 按照较低时延http通信耗时300ms，每次循环将消耗600ms, 而且这个数字将随着NFT数量增加线性增加，我们的页面耗时5、6s是因为我们的NFT只mint了10个左右，正常上线的NFT一般会有接近1万个toknId，那么整体耗时将达到 <strong>100分钟</strong>，这样的速度没有任何用户可以容忍。所以我们一定需要其他方案来优化这一流程。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/76030a307479feb8547d9a0bd6420ee2ac98c3500e85d1085a0b47325165beaa.jpg" alt="6408.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6408.jpeg</figcaption></figure><p>理想状态下前端页面只需要1次请求获取所需要的数据即可，而不是进行多次请求。这就需要1个链下的后端服务帮我们去做类似的逻辑并存储数据，待前端页面需要展示时请求这个后端服务中存储的数据，而TheGraph就是这样的一种服务，与我们自行搭建的后端服务不同，TheGraph是去中心化的，这极大的避免了我们单节点的中心化风险。</p><h3 id="h-thegraph" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">TheGraph工作原理简介</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/20285c24abce332e25857007b3bbbf412fde900e8ee136033e5be3c6314d0d58.jpg" alt="6409.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6409.jpeg</figcaption></figure><h4 id="h-event" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">以太坊事件event</h4><p>之前在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;token=1272524985&amp;lang=zh_CN#rd">以太坊技术系列-以太坊数据结构</a> 中介绍过以太坊中有3棵树，状态树，交易树，收据树。以太坊的事件存储在了收据树中。在智能合约中写入1个事件的方式为使用event关键字定义，使用emit关键字发送。</p><pre data-type="codeBlock" text="//定义事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

//发送事件
emit Transfer(owner, address(0), tokenId);
"><code><span class="hljs-comment">//定义事件</span>
<span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Transfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> <span class="hljs-keyword">from</span>, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> to, <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">indexed</span> tokenId</span>)</span>;

<span class="hljs-comment">//发送事件</span>
<span class="hljs-keyword">emit</span> Transfer(owner, <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), tokenId);
</code></pre><h4 id="h-graphql" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">GraphQL简介</h4><p>GraphQL主要为我们提供了较为细粒度的与服务端交互方式，相比之前的REST API，有如下几个优点:</p><ol><li><p>减少字段冗余，只返回查询中需要的字段。</p></li><li><p>避免多次请求数据，如第一次请求获取id 第二次请求通过id获取其他属性的场景可以一次返回。</p></li><li><p>更有利于前后端分离，前端只需要在请求中增加字段即可获得新增字段，不需要后端配合。</p></li></ol><p>1个GraphQL请求-响应如下所示 请求:</p><pre data-type="codeBlock" text="{
  metaDatas(first:1) {
    id
    name
    image
    owner
  }
}
"><code>{
  <span class="hljs-title function_">metaDatas</span>(<span class="hljs-params">first:<span class="hljs-number">1</span></span>) {
    id
    name
    image
    owner
  }
}
</code></pre><p>响应:</p><pre data-type="codeBlock" text="{
  &quot;data&quot;: {
    &quot;metaDatas&quot;: [
      {
        &quot;id&quot;: &quot;0&quot;,
        &quot;name&quot;: &quot;nft-web3-explorer&quot;,
        &quot;image&quot;: &quot;ipfs://xxx/0.png&quot;,
        &quot;owner&quot;: &quot;0xxxx&quot;
      }
    ]
  }
}
"><code><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"data"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">"metaDatas"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0"</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"name"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"nft-web3-explorer"</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"image"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"ipfs://xxx/0.png"</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"owner"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0xxxx"</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">目标梳理</h2><p>接下来介绍我们如何通过TheGraph来优化NFT列表展示的。具体我们分为如下3步:</p><ol><li><p>确定合约中需要使用的事件, 因为TheGraph是基于以太坊中的事件触发的，所以我们需要在合约中合适的位置发送事件。</p></li><li><p>完成后端代码-TheGraph对于事件的处理并进行存储，我们需要定义数据的存储结构，并编写处理函数在接受到事件的时候进行处理，将数据按照定义结构进行存储。</p></li><li><p>完成前端代码，请求获取TheGraph的数据，我们通过GraphQL的方式从存储中获取需要的数据。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约中加入事件</h2><p>既然触发源是以太坊中的event，那么我们肯定需要在合适的时机发送event，由于我们采用ERC721的实现，查看代码会在mint的时候会发送Transfer event，event中带有mint地址以及tokenId，符合我们的需求，所以我们选择使用该event做为我们的触发源。</p><pre data-type="codeBlock" text="function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), &quot;ERC721: mint to the zero address&quot;);
        require(!_exists(tokenId), &quot;ERC721: token already minted&quot;);

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);

        _afterTokenTransfer(address(0), to, tokenId);
    }
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_mint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> </span>{
        <span class="hljs-built_in">require</span>(to <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">"ERC721: mint to the zero address"</span>);
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>_exists(tokenId), <span class="hljs-string">"ERC721: token already minted"</span>);

        _beforeTokenTransfer(<span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), to, tokenId);

        _balances[to] <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        _owners[tokenId] <span class="hljs-operator">=</span> to;

        <span class="hljs-keyword">emit</span> Transfer(<span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), to, tokenId);

        _afterTokenTransfer(<span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), to, tokenId);
    }
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">创建工程</h3><p>到TheGrapha官网申请subGraph，需要连接github。 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/hosted-service">https://thegraph.com/hosted-service</a></p><p>本地工程初始化</p><pre data-type="codeBlock" text="//安装graph脚手架
npm install -g @graphprotocol/graph-cli

//初始化工程，选择合约地址，abi文件(会自动从合约地址读取，读取失败可以本地上传)，网络(我们依旧使用rinkeby测试网络)
graph init &amp;lt;GITHUB_USERNAME&gt;/&amp;lt;SUBGRAPH_NAME&gt; &amp;lt;DIRECTORY&gt;
"><code><span class="hljs-comment">//安装graph脚手架</span>
npm install <span class="hljs-operator">-</span>g @graphprotocol<span class="hljs-operator">/</span>graph<span class="hljs-operator">-</span>cli

<span class="hljs-comment">//初始化工程，选择合约地址，abi文件(会自动从合约地址读取，读取失败可以本地上传)，网络(我们依旧使用rinkeby测试网络)</span>
graph init <span class="hljs-operator">&#x26;</span>lt;GITHUB_USERNAME<span class="hljs-operator">></span><span class="hljs-operator">/</span><span class="hljs-operator">&#x26;</span>lt;SUBGRAPH_NAME<span class="hljs-operator">></span> <span class="hljs-operator">&#x26;</span>lt;DIRECTORY<span class="hljs-operator">></span>
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">定义存储结构</h3><p>在schema.graphql中定义我们的存储结构</p><pre data-type="codeBlock" text="//记录每次的Transfer事件(非必要)
type QLTransfer @entity {
  id: ID!
  from: Bytes! # address
  to: Bytes! # address
  tokenId: BigInt!
  tokenURI: String!
}

//记录Metadata数据
type MetaData @entity {
  //每条数据需要唯一id，这里我们使用tokenId
  id: ID!
  //持有tokenId的地址
  owner:Bytes!
  //metadata文件中的name
  name: String
  //metadata文件中的image
  image: String
}
"><code><span class="hljs-comment">//记录每次的Transfer事件(非必要)</span>
<span class="hljs-keyword">type</span> QLTransfer @entity {
  id: ID<span class="hljs-operator">!</span>
  <span class="hljs-keyword">from</span>: Bytes<span class="hljs-operator">!</span> # <span class="hljs-keyword">address</span>
  to: Bytes<span class="hljs-operator">!</span> # <span class="hljs-keyword">address</span>
  tokenId: BigInt<span class="hljs-operator">!</span>
  tokenURI: String<span class="hljs-operator">!</span>
}

<span class="hljs-comment">//记录Metadata数据</span>
<span class="hljs-keyword">type</span> MetaData @entity {
  <span class="hljs-comment">//每条数据需要唯一id，这里我们使用tokenId</span>
  id: ID<span class="hljs-operator">!</span>
  <span class="hljs-comment">//持有tokenId的地址</span>
  owner:Bytes<span class="hljs-operator">!</span>
  <span class="hljs-comment">//metadata文件中的name</span>
  name: String
  <span class="hljs-comment">//metadata文件中的image</span>
  image: String
}
</code></pre><p>通过定义的存储结构生成对应的ts代码以方便逻辑代码中直接调用。</p><pre data-type="codeBlock" text="graph codegen
"><code></code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">完成事件处理的逻辑处理</h3><p>在mapping.ts中完成我们的逻辑处理，使用AssemblyScript编写(AssemblyScript是TypeScript的子集，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.assemblyscript.org/">AssemblyScript规范</a>参考)</p><pre data-type="codeBlock" text="export function handleTransfer(event: Transfer): void {
  const qlTransfer = new QLTransfer(event.transaction.hash.toHexString());
  qlTransfer.from = event.params.from;
  qlTransfer.to = event.params.to;
  qlTransfer.tokenId = event.params.tokenId;

  const contract = NFT_WEB3_EXPOLRER.bind(event.address);
  //与合约交互获取tokenId对应的tokenURI
  qlTransfer.tokenURI = contract.tokenURI(event.params.tokenId);
  qlTransfer.save();
  log.info(&apos;qlTransfer id is {}&apos;, [qlTransfer.id]);

  //将http转换为ipfs数据
  const splitstr = qlTransfer.tokenURI.split(&quot;/ipfs/&quot;);
  if(splitstr.length &amp;lt; 2) {
      return;
  }
  const ipfsPath = splitstr[1];
  log.info(&apos;ipfsPath is {}&apos;, [ipfsPath]);
  //获取metadata数据
  const data = ipfs.cat(ipfsPath)
  if (!data) {
    return;
  }
  log.error(&apos;data is {}&apos;, [data.toString()]);
  //转换为json格式
  const value = json.fromBytes(data);
  //新建MetaData结构
  const meta = new MetaData(qlTransfer.tokenId.toString());
  const obj = value.toObject();
  if (obj != null) {
    //解析name
    const name = obj.get(&quot;name&quot;)
    if (name != null) {
      meta.name = name.toString();
    }
    //解析image
    const image = obj.get(&quot;image&quot;)
    if (image != null) {
      meta.image = image.toString();
    }
  }
  meta.owner = qlTransfer.to;
  //数据存储
  meta.save();
  log.info(&apos;meta id is {}&apos;, [meta.id]);
}
"><code>export <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleTransfer</span>(<span class="hljs-params"><span class="hljs-keyword">event</span>: Transfer</span>): <span class="hljs-title">void</span> </span>{
  const qlTransfer <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> QLTransfer(<span class="hljs-keyword">event</span>.transaction.hash.toHexString());
  qlTransfer.from <span class="hljs-operator">=</span> <span class="hljs-keyword">event</span>.params.from;
  qlTransfer.to <span class="hljs-operator">=</span> <span class="hljs-keyword">event</span>.params.to;
  qlTransfer.tokenId <span class="hljs-operator">=</span> <span class="hljs-keyword">event</span>.params.tokenId;

  const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title">NFT_WEB3_EXPOLRER</span>.<span class="hljs-title">bind</span>(<span class="hljs-params"><span class="hljs-keyword">event</span>.<span class="hljs-keyword">address</span></span>);
  <span class="hljs-comment">//与合约交互获取tokenId对应的tokenURI</span>
  <span class="hljs-title">qlTransfer</span>.<span class="hljs-title">tokenURI</span> = <span class="hljs-title"><span class="hljs-keyword">contract</span></span>.<span class="hljs-title">tokenURI</span>(<span class="hljs-params"><span class="hljs-keyword">event</span>.params.tokenId</span>);
  <span class="hljs-title">qlTransfer</span>.<span class="hljs-title">save</span>(<span class="hljs-params"></span>);
  <span class="hljs-title">log</span>.<span class="hljs-title">info</span>(<span class="hljs-params"><span class="hljs-string">'qlTransfer id is {}'</span>, [qlTransfer.id]</span>);

  <span class="hljs-comment">//将http转换为ipfs数据</span>
  <span class="hljs-title">const</span> <span class="hljs-title">splitstr</span> = <span class="hljs-title">qlTransfer</span>.<span class="hljs-title">tokenURI</span>.<span class="hljs-title">split</span>(<span class="hljs-params"><span class="hljs-string">"/ipfs/"</span></span>);
  <span class="hljs-title"><span class="hljs-keyword">if</span></span>(<span class="hljs-params">splitstr.length &#x26;lt; <span class="hljs-number">2</span></span>) </span>{
      <span class="hljs-keyword">return</span>;
  }
  const ipfsPath <span class="hljs-operator">=</span> splitstr[<span class="hljs-number">1</span>];
  log.info(<span class="hljs-string">'ipfsPath is {}'</span>, [ipfsPath]);
  <span class="hljs-comment">//获取metadata数据</span>
  const data <span class="hljs-operator">=</span> ipfs.cat(ipfsPath)
  <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>data) {
    <span class="hljs-keyword">return</span>;
  }
  log.error(<span class="hljs-string">'data is {}'</span>, [data.toString()]);
  <span class="hljs-comment">//转换为json格式</span>
  const value <span class="hljs-operator">=</span> json.fromBytes(data);
  <span class="hljs-comment">//新建MetaData结构</span>
  const meta <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> MetaData(qlTransfer.tokenId.toString());
  const obj <span class="hljs-operator">=</span> value.toObject();
  <span class="hljs-keyword">if</span> (obj <span class="hljs-operator">!</span><span class="hljs-operator">=</span> null) {
    <span class="hljs-comment">//解析name</span>
    const name <span class="hljs-operator">=</span> obj.get(<span class="hljs-string">"name"</span>)
    <span class="hljs-keyword">if</span> (name <span class="hljs-operator">!</span><span class="hljs-operator">=</span> null) {
      meta.<span class="hljs-built_in">name</span> <span class="hljs-operator">=</span> name.toString();
    }
    <span class="hljs-comment">//解析image</span>
    const image <span class="hljs-operator">=</span> obj.get(<span class="hljs-string">"image"</span>)
    <span class="hljs-keyword">if</span> (image <span class="hljs-operator">!</span><span class="hljs-operator">=</span> null) {
      meta.image <span class="hljs-operator">=</span> image.toString();
    }
  }
  meta.owner <span class="hljs-operator">=</span> qlTransfer.to;
  <span class="hljs-comment">//数据存储</span>
  meta.save();
  log.info(<span class="hljs-string">'meta id is {}'</span>, [meta.id]);
}
</code></pre><p>完成代码编写后进行编译</p><pre data-type="codeBlock" text="graph build
"><code></code></pre><h3 id="h-thegraph" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">部署到TheGraph服务器</h3><pre data-type="codeBlock" text="//首次部署需要设置key授权
graph auth --product hosted-service {key}

//部署
graph deploy --product hosted-service EXPLORER-OF-WEB3/nft_web3_explorer_subgraph
"><code><span class="hljs-comment">//首次部署需要设置key授权</span>
graph auth <span class="hljs-operator">-</span><span class="hljs-operator">-</span>product hosted<span class="hljs-operator">-</span>service {key}

<span class="hljs-comment">//部署</span>
graph deploy <span class="hljs-operator">-</span><span class="hljs-operator">-</span>product hosted<span class="hljs-operator">-</span>service EXPLORER<span class="hljs-operator">-</span>OF<span class="hljs-operator">-</span>WEB3<span class="hljs-operator">/</span>nft_web3_explorer_subgraph
</code></pre><p>TheGraph官网会有部署进度，因为要扫描所有区块所以会比较慢，部署完成后我们就有了后端数据，接下来前端展示只要去请求该数据即可。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">前端代码实现</h2><p>我们使用apollo这个库来帮助我们完成GraphQL交互。</p><pre data-type="codeBlock" text="添加依赖
npm i @apollo/client graphql
"><code>添加依赖
npm <span class="hljs-selector-tag">i</span> <span class="hljs-keyword">@apollo</span>/client graphql
</code></pre><p>在uitls目录下新建 graphql_utils.js来管理graphql交互</p><pre data-type="codeBlock" text="import {
    ApolloClient,
    InMemoryCache,
    gql
} from &quot;@apollo/client&quot;;

export const getListData = async () =&gt; {
  //建立连接
    const client = new ApolloClient({
        uri: &quot;thegrapha项目地址&quot;,
        cache: new InMemoryCache()
    });
  //按照graphql形式请求
    const data =  await client.query({
        query: gql`
        query res {
            metaDatas{
                id
                name
                image
            }
        }`
    });
    const list = data?.data?.metaDatas;
    console.log(&quot;getListData&quot; + list);
    return list;
}
"><code><span class="hljs-keyword">import</span> {
    <span class="hljs-title">ApolloClient</span>,
    <span class="hljs-title">InMemoryCache</span>,
    <span class="hljs-title">gql</span>
} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@apollo/client"</span>;

export const getListData <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
  <span class="hljs-comment">//建立连接</span>
    const client <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ApolloClient({
        uri: <span class="hljs-string">"thegrapha项目地址"</span>,
        cache: <span class="hljs-keyword">new</span> InMemoryCache()
    });
  <span class="hljs-comment">//按照graphql形式请求</span>
    const data <span class="hljs-operator">=</span>  await client.query({
        query: gql`
        query res {
            metaDatas{
                id
                name
                image
            }
        }`
    });
    const list <span class="hljs-operator">=</span> data?.data?.metaDatas;
    console.log(<span class="hljs-string">"getListData"</span> <span class="hljs-operator">+</span> list);
    <span class="hljs-keyword">return</span> list;
}
</code></pre><h3 id="h-nft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">获取属于自己NFT</h3><p>如果我们增加1个功能，展示属于自己的NFT，那么只需要在查询中指定owenr为自己的地址即可，如下所示</p><pre data-type="codeBlock" text="{
  metaDatas(where:{owner:&quot;地址&quot;}) {
    id
    name
    image
  }
}
"><code>{
  <span class="hljs-title function_">metaDatas</span>(<span class="hljs-params">where:{owner:<span class="hljs-string">"地址"</span>}</span>) {
    id
    name
    image
  }
}
</code></pre><p>在使用TheGrpha后，我们的NFT列表展示速度稳定控制在500ms左右，极大地提升了用户体验，相对于直接与合约交互耗时降低了1万多倍。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本文我们从NFT列表展示速度慢的现象开始分析，发现前端展示流程不合理的地方，通过引入TheGraph将NFT列表展示耗时降低了1万多倍，实际中还有很多类似的应用。由于需要尽可能地减少以太坊中存储，我们将部分数据存入链下而且即使在以太坊中的存储也可能没有合适的索引，只能前端拿到所有数据再建立索引(比如属于某个地址的tokenId集合)。这样的情况下我们就需要后端服务来帮助我们建立链上数据的索引并整合链下数据，可以极大地优化用户体验。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/4ef4c9e400558f69497b8f7071f8eb997a437ecf8f5d4841f831dd4e10270150.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[像StepN一样在Dapp中内置钱包]]></title>
            <link>https://paragraph.com/@explorer-of-web3/stepn-dapp</link>
            <guid>ojpa3TCUd76kgjELcnze</guid>
            <pubDate>Sun, 08 May 2022 13:19:55 GMT</pubDate>
            <description><![CDATA[目前的钱包(包括MetaMask)覆盖用户程度不高，可能会增加用户的使用门槛，这也是StepN内置钱包的原因(StepN所处的solana链钱包覆盖度更低)，本文将介绍如何在我们的DApp中内置钱包。 在如何发行一款NFT(下)的官方搭建中，我们支持了MetaMask钱包，但如果是一款希望能够"破圈"的应用，目前的钱包(包括MetaMask)覆盖用户程度不高，可能会增加用户的使用门槛，这也是StepN内置钱包的原因(StepN所处的solana链钱包覆盖度更低)，本文将介绍如何在我们的DApp中内置钱包。钱包基础概念581.jpeg如上图所示，私钥可以计算出公钥，公钥可以计算出钱包账户地址，每一步都是单向可行，比如，通过公钥是无法计算出私钥的。所以对于钱包账户，我们的核心数据只有1个，那就是私钥，有了私钥我们就可以推导公钥和钱包账户地址。 钱包内的余额等信息均存储于链上，链上存储着钱包账户地址->余额的映射，如果发生转账等操作需要进行私钥签名，公钥验证。目标规划1.连接钱包: 用户需要能够连接到我们的内置钱包，同时支持新创建钱包账户并连接或者导入之前已创建的钱包账户并连接两种方式...]]></description>
            <content:encoded><![CDATA[<p>目前的钱包(包括MetaMask)覆盖用户程度不高，可能会增加用户的使用门槛，这也是StepN内置钱包的原因(StepN所处的solana链钱包覆盖度更低)，本文将介绍如何在我们的DApp中内置钱包。</p><p>在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483910&amp;idx=1&amp;sn=ec677661e36d2aeaf25b21f193864834&amp;chksm=c1c9940cf6be1d1ab09b8a9c8290011df557b61585fd335c23a618136ca7284d6d701fd75fb2#rd">如何发行一款NFT(下)</a>的官方搭建中，我们支持了MetaMask钱包，但如果是一款希望能够&quot;破圈&quot;的应用，目前的钱包(包括MetaMask)覆盖用户程度不高，可能会增加用户的使用门槛，这也是StepN内置钱包的原因(StepN所处的solana链钱包覆盖度更低)，本文将介绍如何在我们的DApp中内置钱包。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">钱包基础概念</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d8884c83dd1b0e22e767e7a658ee794b3b91890f83ef88797dffefb4cfa055e2.jpg" alt="581.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">581.jpeg</figcaption></figure><p>如上图所示，私钥可以计算出公钥，公钥可以计算出钱包账户地址，每一步都是单向可行，比如，通过公钥是无法计算出私钥的。所以对于钱包账户，我们的核心数据只有1个，那就是私钥，有了私钥我们就可以推导公钥和钱包账户地址。 钱包内的余额等信息均存储于链上，链上存储着钱包账户地址-&gt;余额的映射，如果发生转账等操作需要进行私钥签名，公钥验证。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">目标规划</h2><p>1.连接钱包: 用户需要能够连接到我们的内置钱包，同时支持新创建钱包账户并连接或者导入之前已创建的钱包账户并连接两种方式。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7e7a13821a986a5212da7266d23b72a7a25a987a0a262586b49c7cb93ea783ef.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>2. 展示已连接钱包账户信息，包括钱包账户地址，余额，私钥(默认不展示)，并支持断开已连接的钱包账户。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b76acde149c53e5828c21fdb38067b89ea9d420c42ad53100e9b85d4373eaf5f.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>3.使用已连接钱包账户完成转账，并支持在转账前进行二次弹窗确认。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0670a4484ff3ee9c8e45e4d44ebf04182d41ceebcddbd1ef12d3df1b9e0e5851.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">技术选型</h2><ul><li><p>基础框架:由于我们是在之前的NFT官网基础上继续完善，所以我们依旧还是使用react+mobx+ether的组合。</p></li><li><p>持久化:由于我们需要持久化保存钱包账户的连接，避免页面刷新连接断开，所以我们使用localStorage持久化已连接的钱包账户。</p></li><li><p>节点代理(管理节点):由于我们不再使用metamask，所以我们需要自己去连接以太坊节点来进行链上操作，在如何发行一款NFT(上)中我们介绍过Infura，我们继续使用Infura作为节点服务代理。本文不再介绍如何申请Key相关，如果有疑问可以参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483877&amp;idx=1&amp;sn=9036dbe8bfbb95aba99520b4e1800509&amp;chksm=c1c997eff6be1ef99b8bd8ed30f86edfbd1a42e8aa9c6a2bceb9e40f0c40effe8fdd02b33faf#rd">如何发行一款NFT(上)</a>中的介绍。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">连接钱包</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">创建新钱包账户并连接</h3><p>在wallet_utils.js中新增创建钱包账户，连接钱包的方法。创建钱包本质就是寻找1个随机数作为私钥，并使用这个私钥生成公钥和钱包地址。</p><pre data-type="codeBlock" text="//创建一个新的钱包账户
export const createNewWallet = (): Wallet =&gt; {
    const wallet = ethers.Wallet.createRandom();
    return wallet;
}

const NETWORK = &quot;rinkeby&quot;
const API_KEY = &quot;在infura中申请的key&quot;

//连接内置钱包账户
export const connectCustomWallet =  (wallet:Wallet) : Wallet =&gt; {
    const provider = new ethers.providers.InfuraProvider(NETWORK, API_KEY);
    const conncetWallet = wallet.connect(provider);
    return conncetWallet;
}
"><code><span class="hljs-comment">//创建一个新的钱包账户</span>
export const createNewWallet <span class="hljs-operator">=</span> (): Wallet <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const wallet <span class="hljs-operator">=</span> ethers.Wallet.createRandom();
    <span class="hljs-keyword">return</span> wallet;
}

const NETWORK <span class="hljs-operator">=</span> <span class="hljs-string">"rinkeby"</span>
const API_KEY <span class="hljs-operator">=</span> <span class="hljs-string">"在infura中申请的key"</span>

<span class="hljs-comment">//连接内置钱包账户</span>
export const connectCustomWallet <span class="hljs-operator">=</span>  (wallet:Wallet) : Wallet <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.InfuraProvider(NETWORK, API_KEY);
    const conncetWallet <span class="hljs-operator">=</span> wallet.connect(provider);
    <span class="hljs-keyword">return</span> conncetWallet;
}
</code></pre><p>新增内置钱包的组件CustomWalletComponent.js并在App.js中添加，这个组件将会负责管理内置钱包连接页面和展示内置钱包信息页面。</p><pre data-type="codeBlock" text="import { Store } from &apos;../Store&apos;;
import &apos;./wallet-page.css&apos;;
import { observer } from &apos;mobx-react&apos;;
import ConnectCustomWalletComponent from &apos;./ConnectCustomWalletComponent&apos;;
import CustomWalletInfoComponent from &apos;./CustomWalletInfoComponent&apos;;

//使用observer修饰组件，observable的值发生变化组件重新渲染
const CustomWalletComponent = observer(() =&gt; {
    switch (Store.customWalletPageShow.get()) {
        case Custom_Wallet_Page_Show_Type.show_connect_page:
            //内置钱包连接组件
            return &amp;lt;ConnectCustomWalletComponent /&gt;;
        case Custom_Wallet_Page_Show_Type.show_info_page:
            //内置钱包信息组件
            return &amp;lt;CustomWalletInfoComponent/&gt;;
        default:
            //默认不展示
            return &amp;lt;div className=&apos;display-none&apos;&gt;&amp;lt;/div&gt;;
    }
});

export default CustomWalletComponent;

//定义组件展示形态枚举
//hidden: 不展示弹窗   
//show_connect_page:展示内置钱包连接页面
//show_connect_page:展示内置钱包信息页面
export const Custom_Wallet_Page_Show_Type = {
    hidden: 0,
    show_connect_page: 1,
    show_info_page: 2
}
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">Store</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./wallet-page.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">observer</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'mobx-react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">ConnectCustomWalletComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./ConnectCustomWalletComponent'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">CustomWalletInfoComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./CustomWalletInfoComponent'</span>;

<span class="hljs-comment">//使用observer修饰组件，observable的值发生变化组件重新渲染</span>
const CustomWalletComponent <span class="hljs-operator">=</span> observer(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    switch (Store.customWalletPageShow.get()) {
        case Custom_Wallet_Page_Show_Type.show_connect_page:
            <span class="hljs-comment">//内置钱包连接组件</span>
            <span class="hljs-keyword">return</span> <span class="hljs-operator">&#x26;</span>lt;ConnectCustomWalletComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>;
        case Custom_Wallet_Page_Show_Type.show_info_page:
            <span class="hljs-comment">//内置钱包信息组件</span>
            <span class="hljs-keyword">return</span> <span class="hljs-operator">&#x26;</span>lt;CustomWalletInfoComponent<span class="hljs-operator">/</span><span class="hljs-operator">></span>;
        default:
            <span class="hljs-comment">//默认不展示</span>
            <span class="hljs-keyword">return</span> <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'display-none'</span><span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>;
    }
});

export default CustomWalletComponent;

<span class="hljs-comment">//定义组件展示形态枚举</span>
<span class="hljs-comment">//hidden: 不展示弹窗   </span>
<span class="hljs-comment">//show_connect_page:展示内置钱包连接页面</span>
<span class="hljs-comment">//show_connect_page:展示内置钱包信息页面</span>
export const Custom_Wallet_Page_Show_Type <span class="hljs-operator">=</span> {
    hidden: <span class="hljs-number">0</span>,
    show_connect_page: <span class="hljs-number">1</span>,
    show_info_page: <span class="hljs-number">2</span>
}
</code></pre><p>在内置钱包连接组件ConnectCustomWalletComponent中新增我们的创建新钱包账户组件CreateNewCustomWalletComponent。</p><pre data-type="codeBlock" text="import { Store } from &apos;../Store&apos;;
import &apos;./wallet-page.css&apos;;
import { useState } from &apos;react&apos;;
import { connectCustomWallet, createNewWallet } from &apos;../utils/wallet_utils&apos;;
import { Custom_Wallet_Page_Show_Type } from &apos;./CustomWalletComponent&apos;;

const CreateNewCustomWalletComponent = () =&gt; {
    const [address, setAddress] = useState(&quot;&quot;);
    const [privateKey, setPrivateKey] = useState(&quot;&quot;);
    const [wallet, setWallet] = useState(null);
    //调用createNewWallet创建新钱包账户，并展示其公钥和私钥，暂存wallet
    const create_wallet_click = () =&gt; {
        const wallet = createNewWallet()
        setWallet(wallet);
        setPrivateKey(wallet.privateKey);
        setAddress(wallet.address);
    }
    //调用connectCustomWallet连接钱包账户，并更新全局状态
    const connect_wallet_click = () =&gt; {
        const connctWallet = connectCustomWallet(wallet);
        Store.updataCustomWallet(connctWallet);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }
    return (
        &amp;lt;div className=&apos;custom-wallet-page-panel-item-create&apos;&gt;
            &amp;lt;h2 &gt;创建新账户&amp;lt;/h2&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-button&apos; onClick={e =&gt; { create_wallet_click() }}&gt; 创建账户 &amp;lt;/div&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;账户地址&amp;lt;/div&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;{address}&amp;lt;/div&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;私钥:&amp;lt;/div&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;{privateKey}&amp;lt;/div&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-button&apos; onClick={e =&gt; { connect_wallet_click() }}&gt; 连接钱包 &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
};

export default CreateNewCustomWalletComponent;
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">Store</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./wallet-page.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">connectCustomWallet</span>, <span class="hljs-title">createNewWallet</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../utils/wallet_utils'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Custom_Wallet_Page_Show_Type</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./CustomWalletComponent'</span>;

const CreateNewCustomWalletComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const [<span class="hljs-keyword">address</span>, setAddress] <span class="hljs-operator">=</span> useState(<span class="hljs-string">""</span>);
    const [privateKey, setPrivateKey] <span class="hljs-operator">=</span> useState(<span class="hljs-string">""</span>);
    const [wallet, setWallet] <span class="hljs-operator">=</span> useState(null);
    <span class="hljs-comment">//调用createNewWallet创建新钱包账户，并展示其公钥和私钥，暂存wallet</span>
    const create_wallet_click <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        const wallet <span class="hljs-operator">=</span> createNewWallet()
        setWallet(wallet);
        setPrivateKey(wallet.privateKey);
        setAddress(wallet.<span class="hljs-built_in">address</span>);
    }
    <span class="hljs-comment">//调用connectCustomWallet连接钱包账户，并更新全局状态</span>
    const connect_wallet_click <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        const connctWallet <span class="hljs-operator">=</span> connectCustomWallet(wallet);
        Store.updataCustomWallet(connctWallet);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-create'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h2 <span class="hljs-operator">></span>创建新账户<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h2<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-button'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { create_wallet_click() }}<span class="hljs-operator">></span> 创建账户 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>账户地址<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>{<span class="hljs-keyword">address</span>}<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>私钥:<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>{privateKey}<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-button'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { connect_wallet_click() }}<span class="hljs-operator">></span> 连接钱包 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
};

export default CreateNewCustomWalletComponent;
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">管理钱包账户状态</h3><p>钱包账户状态是1个全局状态，而且页面退出再进入也应该保持连接状态，所以我们将钱包账户持久化到本地，并在页面初始化的时候读取本地存储的钱包账户。那么钱包账户里面信息很多，我们是否需要全部存储？按照我们开始介绍的钱包基础概念，钱包账户核心就是私钥，只要有私钥我们就可以计算得到钱包账户的其他信息，所以我们持久化私钥即可。 先在wallet_utils.js中新增通过私钥生成钱包账户的方法</p><pre data-type="codeBlock" text="//通过私钥恢复钱包账户其他信息
export const restoreConnectCustomWallet = (privateKey) =&gt; {
    const wallet = new ethers.Wallet(privateKey);
    const provider = new ethers.providers.InfuraProvider(NETWORK, API_KEY);
    const conncetWallet = wallet.connect(provider);
    return conncetWallet;
}
"><code><span class="hljs-comment">//通过私钥恢复钱包账户其他信息</span>
export const restoreConnectCustomWallet <span class="hljs-operator">=</span> (privateKey) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const wallet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Wallet(privateKey);
    const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.InfuraProvider(NETWORK, API_KEY);
    const conncetWallet <span class="hljs-operator">=</span> wallet.connect(provider);
    <span class="hljs-keyword">return</span> conncetWallet;
}
</code></pre><p>我们在Store.js中对本地钱包账户做一次缓存，这样方便页面内状态监听。</p><pre data-type="codeBlock" text="const customWallet = observable.box(null);

//初始化方法
const init = () =&gt; {
    //判断是否本地有存储内置钱包账户私钥，如果有则恢复连接，没有则开始监听MetaMask
    const privateKey = localStorage.getItem(&quot;wallet_private_key&quot;);
    if (privateKey) {
            console.log(&quot;already connect &quot; + privateKey);
            customWallet.set(restoreConnectCustomWallet(privateKey));
    } else {
        registerAccountChange();
    }
}      

//更新钱包账户，同时更新Store.js缓存和本地存储。
const updataCustomWallet = (wallet) =&gt; {
    customWallet.set(wallet);
    localStorage.setItem(&quot;wallet_private_key&quot;, wallet ? wallet.privateKey : &quot;&quot;);
   }
"><code>const customWallet <span class="hljs-operator">=</span> observable.box(null);

<span class="hljs-comment">//初始化方法</span>
const init <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-comment">//判断是否本地有存储内置钱包账户私钥，如果有则恢复连接，没有则开始监听MetaMask</span>
    const privateKey <span class="hljs-operator">=</span> localStorage.getItem(<span class="hljs-string">"wallet_private_key"</span>);
    <span class="hljs-keyword">if</span> (privateKey) {
            console.log(<span class="hljs-string">"already connect "</span> <span class="hljs-operator">+</span> privateKey);
            customWallet.set(restoreConnectCustomWallet(privateKey));
    } <span class="hljs-keyword">else</span> {
        registerAccountChange();
    }
}      

<span class="hljs-comment">//更新钱包账户，同时更新Store.js缓存和本地存储。</span>
const updataCustomWallet <span class="hljs-operator">=</span> (wallet) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    customWallet.set(wallet);
    localStorage.setItem(<span class="hljs-string">"wallet_private_key"</span>, wallet ? wallet.privateKey : <span class="hljs-string">""</span>);
   }
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">调整合约交互</h3><p>我们之前构建合约的时候都是默认使用MetaMask，但目前引入了内置钱包，我们需要进行区分构建，在contract_utils.js进行调整</p><pre data-type="codeBlock" text="const getContract = () =&gt; {
    const address = &quot;合约地址&quot;;
    const abi = require(&quot;../abi/NFT_WEB3_EXPOLRER.json&quot;).abi;
    return new ethers.Contract(address, abi, getProvider());
}

const getProvider = () =&gt; {
    //是否有已经连接的内置钱包账号，有则使用Infura节点代理，没有则使用MetaMask
    if (Store.customWallet.get()) {
        return new ethers.providers.InfuraProvider(NETWORK, API_KEY);
    } else {
        return new ethers.providers.Web3Provider(window.web3.currentProvider);
    }
}
"><code>const getContract <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const <span class="hljs-keyword">address</span> <span class="hljs-operator">=</span> <span class="hljs-string">"合约地址"</span>;
    const <span class="hljs-built_in">abi</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"../abi/NFT_WEB3_EXPOLRER.json"</span>).abi;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ethers.Contract(<span class="hljs-keyword">address</span>, <span class="hljs-built_in">abi</span>, getProvider());
}

const getProvider <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-comment">//是否有已经连接的内置钱包账号，有则使用Infura节点代理，没有则使用MetaMask</span>
    <span class="hljs-keyword">if</span> (Store.customWallet.get()) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ethers.providers.InfuraProvider(NETWORK, API_KEY);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ethers.providers.Web3Provider(window.web3.currentProvider);
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">导入钱包账户</h3><p>如果我们已经拥有1个钱包账户需要导入到内置钱包中，这一过程其实和我们页面初始化时从本地读取钱包账户一致，都是需要通过私钥恢复完整的钱包账户。</p><pre data-type="codeBlock" text="import { Store } from &apos;../Store&apos;;
import &apos;./wallet-page.css&apos;;
import { useState } from &apos;react&apos;;
import { Custom_Wallet_Page_Show_Type } from &apos;./CustomWalletComponent&apos;;

const ImportCustomWalletComponent = () =&gt; {
    const [inputValue, setInputValue] = useState(&quot;&quot;);

    //通过用户输入的私钥导入钱包账户，并更新钱包账户的存储
    const import_wallet_click = () =&gt; {
        const connctWallet = restoreConnectCustomWallet(inputValue);
        Store.updataCustomWallet(connctWallet);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }

    const handleChange = (event) =&gt; {
        console.log(&quot;input value is&quot; + event.target.value);
        setInputValue(event.target.value);
    }
    return (
        &amp;lt;div className=&apos;custom-wallet-page-panel-item-import&apos; &gt;
            &amp;lt;h2&gt;导入现有账户&amp;lt;/h2&gt;
            &amp;lt;h4&gt;请输入已有账户私钥&amp;lt;/h4&gt;
            &amp;lt;input type=&apos;text&apos; value={inputValue} onChange={handleChange}&gt;&amp;lt;/input&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-button&apos; onClick={e =&gt; { import_wallet_click() }}&gt; 导入账户并连接 &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
};

export default ImportCustomWalletComponent;
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">Store</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./wallet-page.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Custom_Wallet_Page_Show_Type</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./CustomWalletComponent'</span>;

const ImportCustomWalletComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const [inputValue, setInputValue] <span class="hljs-operator">=</span> useState(<span class="hljs-string">""</span>);

    <span class="hljs-comment">//通过用户输入的私钥导入钱包账户，并更新钱包账户的存储</span>
    const import_wallet_click <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        const connctWallet <span class="hljs-operator">=</span> restoreConnectCustomWallet(inputValue);
        Store.updataCustomWallet(connctWallet);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }

    const handleChange <span class="hljs-operator">=</span> (<span class="hljs-function"><span class="hljs-keyword">event</span>) => </span>{
        console.log(<span class="hljs-string">"input value is"</span> <span class="hljs-operator">+</span> <span class="hljs-keyword">event</span>.target.<span class="hljs-built_in">value</span>);
        setInputValue(<span class="hljs-keyword">event</span>.target.<span class="hljs-built_in">value</span>);
    }
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-import'</span> <span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h2<span class="hljs-operator">></span>导入现有账户<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h2<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h4<span class="hljs-operator">></span>请输入已有账户私钥<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h4<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;input <span class="hljs-keyword">type</span><span class="hljs-operator">=</span><span class="hljs-string">'text'</span> value<span class="hljs-operator">=</span>{inputValue} onChange<span class="hljs-operator">=</span>{handleChange}<span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>input<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-button'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { import_wallet_click() }}<span class="hljs-operator">></span> 导入账户并连接 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
};

export default ImportCustomWalletComponent;
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">展示已连接钱包账户信息</h2><p>之前我们页面右上角的连接钱包入口在连接完成后设定是无法点击的，现在有了内置钱包，我们将其修改为连接内置钱包账户时点击打开内置钱包账户的信息展示页面，展示钱包账户地址，余额，私钥(默认不展示)，并在该页面中提供断开钱包账户连接的功能。其中按照开始介绍的钱包基础概念，钱包账户余额存在链上，所以获取余额需要请求节点代理是1个异步过程。断开钱包账户连接其实就是清空本地存储的私钥信息。</p><pre data-type="codeBlock" text="import { Store } from &apos;../Store&apos;;
import &apos;./wallet-page.css&apos;;
import { useEffect, useState } from &apos;react&apos;;
import { Custom_Wallet_Page_Show_Type } from &apos;./CustomWalletComponent&apos;;

const CustomWalletInfoComponent = () =&gt; {
    const [balance, setBalance] = useState(&quot;&quot;);
    const [isShowPrivateKey, setShowPrivateKey] = useState(false);

    useEffect(() =&gt; {
        //获取当前内置钱包账户的余额，需要请求节点代理
        Store.customWallet.get().getBalance()
            .then((b) =&gt; {
                console.log(b);
                setBalance(b?.toString());
            })
            .catch((e) =&gt; {
                console.error(e);
            });
    }, []);

    //私钥默认不展示
    const show_private_key = () =&gt; {
        setShowPrivateKey(true);
    }

    //断开连接，清空保存的账户信息
    const disconncet = () =&gt; {
        Store.updataCustomWallet(null);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }

    console.log(&quot;privateKey&quot; + Store.customWallet.get()?.privateKey);
    return (&amp;lt;div className=&apos;wallet-page-bg&apos; onClick={() =&gt; { cancel_click() }}&gt;
        &amp;lt;div className=&apos;custom-wallet-page-panel-info&apos; onClick={e =&gt; { panel_click(e) }}&gt;
            &amp;lt;div className=&apos;custom-wallet-page-panel-item-info&apos; &gt;
                &amp;lt;h2 &gt;账户信息&amp;lt;/h2&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;钱包地址:&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;{Store.customWallet.get()?.address}&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;余额:&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;{balance}wei&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-button&apos; onClick={e =&gt; { show_private_key() }}&gt; 显示私钥 &amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;私钥:&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-text&apos;&gt;{isShowPrivateKey ? Store.customWallet.get()?.privateKey : &quot;&quot;}&amp;lt;/div&gt;
                &amp;lt;div className=&apos;custom-wallet-page-panel-item-button&apos; onClick={e =&gt; { disconncet() }}&gt; 断开连接 &amp;lt;/div&gt;
            &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    &amp;lt;/div&gt;);

};

export default CustomWalletInfoComponent;
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">Store</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./wallet-page.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">useEffect</span>, <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Custom_Wallet_Page_Show_Type</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./CustomWalletComponent'</span>;

const CustomWalletInfoComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const [balance, setBalance] <span class="hljs-operator">=</span> useState(<span class="hljs-string">""</span>);
    const [isShowPrivateKey, setShowPrivateKey] <span class="hljs-operator">=</span> useState(<span class="hljs-literal">false</span>);

    useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        <span class="hljs-comment">//获取当前内置钱包账户的余额，需要请求节点代理</span>
        Store.customWallet.get().getBalance()
            .then((b) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                console.log(b);
                setBalance(b?.toString());
            })
            .catch((e) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                console.error(e);
            });
    }, []);

    <span class="hljs-comment">//私钥默认不展示</span>
    const show_private_key <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        setShowPrivateKey(<span class="hljs-literal">true</span>);
    }

    <span class="hljs-comment">//断开连接，清空保存的账户信息</span>
    const disconncet <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        Store.updataCustomWallet(null);
        Store.customWalletPageShow.set(Custom_Wallet_Page_Show_Type.hidden);
    }

    console.log(<span class="hljs-string">"privateKey"</span> <span class="hljs-operator">+</span> Store.customWallet.get()?.privateKey);
    <span class="hljs-keyword">return</span> (<span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'wallet-page-bg'</span> onClick<span class="hljs-operator">=</span>{() <span class="hljs-operator">=</span><span class="hljs-operator">></span> { cancel_click() }}<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-info'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { panel_click(e) }}<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-info'</span> <span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;h2 <span class="hljs-operator">></span>账户信息<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h2<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>钱包地址:<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>{Store.customWallet.get()?.<span class="hljs-built_in">address</span>}<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>余额:<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>{balance}<span class="hljs-literal">wei</span><span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-button'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { show_private_key() }}<span class="hljs-operator">></span> 显示私钥 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>私钥:<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-text'</span><span class="hljs-operator">></span>{isShowPrivateKey ? Store.customWallet.get()?.privateKey : <span class="hljs-string">""</span>}<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'custom-wallet-page-panel-item-button'</span> onClick<span class="hljs-operator">=</span>{e <span class="hljs-operator">=</span><span class="hljs-operator">></span> { disconncet() }}<span class="hljs-operator">></span> 断开连接 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>);

};

export default CustomWalletInfoComponent;
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">使用已连接钱包完成转账</h2><p>在我们的NFT官网中mint是一种转账操作，该操作需要向合约账户转一定数量的ETH，转账操作会需要私钥的签名，之前我们默认使用MetaMask的Singer，现在在contract_utils.js中新增方法，根据是否连接内置钱包选择不同的Singer。补充一点，Wallet继承自Singer，所以Wallet可以直接作为Singer使用。</p><pre data-type="codeBlock" text="const getSigner = () =&gt; {
    //已连接内置钱包，则优先使用内置钱包的账户
    if (Store.customWallet.get()) {
        return Store.customWallet.get();
    } else {
        const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
        return provider.getSigner();
    }
}
"><code>const getSigner <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-comment">//已连接内置钱包，则优先使用内置钱包的账户</span>
    <span class="hljs-keyword">if</span> (Store.customWallet.get()) {
        <span class="hljs-keyword">return</span> Store.customWallet.get();
    } <span class="hljs-keyword">else</span> {
        const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.Web3Provider(window.web3.currentProvider);
        <span class="hljs-keyword">return</span> provider.getSigner();
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">转账对用户进行提示</h3><p>在官网进行转账操作，我们有义务提示用户进行二次确认，避免在用户不知情的情况下发生转账行为给用户带来资金损失，所以修改contract_utils.js中的mint方法，在执行Mint这一转账操作前，弹出弹窗让用户进行确认。</p><pre data-type="codeBlock" text="export const mint = async () =&gt; {
    const contract = getContract();
    const contractWithSigner = contract.connect(getSigner());
    const price = await contract.PRICE_PER_TOKEN();
    console.log(&quot;price is&quot; + price);
    //调用Mint方法前先弹确认弹窗让用户进行确认
    const r = window.confirm(&quot;你将向地址: &quot;+ contract.address + &quot;  转账: &quot; + price +&quot;wei&quot;);
    if (r) {
        const tx = await contractWithSigner.mint(1, { value: price });
        console.log(tx);
        await tx.wait();
        window.alert(&quot;mint成功&quot;);
    }
}
"><code>export const mint <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title">getContract</span>(<span class="hljs-params"></span>);
    <span class="hljs-title">const</span> <span class="hljs-title">contractWithSigner</span> = <span class="hljs-title"><span class="hljs-keyword">contract</span></span>.<span class="hljs-title">connect</span>(<span class="hljs-params">getSigner(<span class="hljs-params"></span>)</span>);
    <span class="hljs-title">const</span> <span class="hljs-title">price</span> = <span class="hljs-title">await</span> <span class="hljs-title"><span class="hljs-keyword">contract</span></span>.<span class="hljs-title">PRICE_PER_TOKEN</span>(<span class="hljs-params"></span>);
    <span class="hljs-title">console</span>.<span class="hljs-title">log</span>(<span class="hljs-params"><span class="hljs-string">"price is"</span> + price</span>);
    <span class="hljs-comment">//调用Mint方法前先弹确认弹窗让用户进行确认</span>
    <span class="hljs-title">const</span> <span class="hljs-title">r</span> = <span class="hljs-title">window</span>.<span class="hljs-title">confirm</span>(<span class="hljs-params"><span class="hljs-string">"你将向地址: "</span>+ <span class="hljs-keyword">contract</span>.<span class="hljs-keyword">address</span> + <span class="hljs-string">"  转账: "</span> + price +<span class="hljs-string">"wei"</span></span>);
    <span class="hljs-title"><span class="hljs-keyword">if</span></span> (<span class="hljs-params">r</span>) </span>{
        const <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await contractWithSigner.mint(<span class="hljs-number">1</span>, { <span class="hljs-built_in">value</span>: price });
        console.log(<span class="hljs-built_in">tx</span>);
        await <span class="hljs-built_in">tx</span>.wait();
        window.alert(<span class="hljs-string">"mint成功"</span>);
    }
}
</code></pre><p>至此，我们为我们的Dapp增加了1个功能完整的内置钱包。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>由于以太坊中账户校验规则都是一致的，所以使用该内置钱包生成的账户也可连接到MetaMask, 或者将MetaMask生成的账户导入到内置钱包也可以正常使用。此外，为了减少篇幅没有介绍助记词相关，如果有兴趣，可以了解BIP39在本文基础上很容易拓展支持助记词。内置钱包可以减少用户使用Dapp的门槛，希望各位小伙伴早日探索开发出可以破圈的Dapp。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c6d05d490fa3d4cc5dc1bdfb018bc8616b17a7588b40e150fc495ab0ca4953b0.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[如何发行一款NFT(下)]]></title>
            <link>https://paragraph.com/@explorer-of-web3/nft-2</link>
            <guid>bzweby0C1WiV8YTW1oKP</guid>
            <pubDate>Wed, 04 May 2022 10:29:36 GMT</pubDate>
            <description><![CDATA[我们通过2篇文章来介绍NFT的发布，第一篇重点讲解如何上链，可以归属于后端逻辑。第二篇讲解官方搭建，可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例，希望能够帮助到希望进入web3行业的同学。 上篇文章我们介绍了，如何部署NFT的智能合约到链上，这相当于NFT的后端部分完成，但作为NFT的宣发需要1个网站，属于NFT的前端部分。官网规划这个网站需要达成下面这几个目标:项目介绍NFT mint进度展示 以及mint功能NFT 列表展示联系方式为了达成这些目标，我们通常会先完成草图的设计，在这里我直接使用项目最后的截图来展示UI架构，大家可以使用figma等工具完成第一步的草图设计。542.jpeg543.jpeg544.jpeg可以看到，页面可以分为3部分顶部栏: 包含项目名称、主功能tab、连接钱包按钮。内容区: 根据顶部栏所选tab不同分为项目简介页面、mint进度以及mint功能页面、NFT列表展示页面。底部栏: 包含联系方式。同时点击连接钱包按钮，我们需要1个钱包选择页面(目前只支持Me...]]></description>
            <content:encoded><![CDATA[<p>我们通过2篇文章来介绍NFT的发布，第一篇重点讲解如何上链，可以归属于后端逻辑。第二篇讲解官方搭建，可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例，希望能够帮助到希望进入web3行业的同学。</p><p>上篇文章我们介绍了，如何部署NFT的智能合约到链上，这相当于NFT的后端部分完成，但作为NFT的宣发需要1个网站，属于NFT的前端部分。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">官网规划</h2><p>这个网站需要达成下面这几个目标:</p><ol><li><p>项目介绍</p></li><li><p>NFT mint进度展示 以及mint功能</p></li><li><p>NFT 列表展示</p></li><li><p>联系方式</p></li></ol><p>为了达成这些目标，我们通常会先完成草图的设计，在这里我直接使用项目最后的截图来展示UI架构，大家可以使用figma等工具完成第一步的草图设计。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d4843a8daf447f5b7f767783d29e38fc1a287b9c05f28176a4466b30f087448e.jpg" alt="542.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">542.jpeg</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fa45d7205bf22bfccb5641c8863a0600dd11a07ca0528bee6eade7cf64130698.jpg" alt="543.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">543.jpeg</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b7dd580bdf983f55dd7833128466fba2f3018f4b6fd47360e050d76bfe53914d.jpg" alt="544.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">544.jpeg</figcaption></figure><p>可以看到，页面可以分为3部分</p><ul><li><p>顶部栏: 包含项目名称、主功能tab、连接钱包按钮。</p></li><li><p>内容区: 根据顶部栏所选tab不同分为项目简介页面、mint进度以及mint功能页面、NFT列表展示页面。</p></li><li><p>底部栏: 包含联系方式。</p></li></ul><p>同时点击连接钱包按钮，我们需要1个钱包选择页面(目前只支持MetaMask)进行连接。</p><p>其中我们需要重点关注的是连接钱包按钮，钱包选择页面，mint页面, NFT展示页面。其余页面比较简单，为了完整性还是将其余部分代码展现，如果有前端基础的同学只查看上述重点页面实现即可。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">技术选型</h2><p>项目框架: 项目采用Reac框架，页面整体状态由mbox框架来进行管理,我们只使用mbox最简单的用法。</p><p>钱包交互: 我们使用MetaMask API</p><p>合约交互: 使用ethers.js API</p><p>除了上述依赖库外，我们的项目将尽可能减少依赖库的使用</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">初始化项目</h2><pre data-type="codeBlock" text="//使用react 官方脚手架初始化我们的项目
npx create-react-app nft-web3-explorer-page
cd nft-web3-explorer-page

//尝试运行项目
npm start
"><code><span class="hljs-comment">//使用react 官方脚手架初始化我们的项目</span>
npx create<span class="hljs-operator">-</span>react<span class="hljs-operator">-</span>app nft<span class="hljs-operator">-</span>web3<span class="hljs-operator">-</span>explorer<span class="hljs-operator">-</span>page
cd nft<span class="hljs-operator">-</span>web3<span class="hljs-operator">-</span>explorer<span class="hljs-operator">-</span>page

<span class="hljs-comment">//尝试运行项目</span>
npm start
</code></pre><p>使用vscode打开我们的工程</p><p>我们按照页面框架功能区，在src下新建3个目录topbar、content、bottombar，并在3个目录下分别新建组件TopBarComponent.js、ContentComponent.js、BottomBarComponent.js。(css样式大家自行调整,不是本文重点)</p><p>TopBarComponent.js</p><pre data-type="codeBlock" text="import &apos;./topbar.css&apos;;

const TopBarComponent = () =&gt; {
  return (
    &amp;lt;div className=&apos;topbar&apos;&gt;

    &amp;lt;/div&gt;
  );
}
export default TopBarComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./topbar.css'</span>;

const TopBarComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
  <span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'topbar'</span><span class="hljs-operator">></span>

    <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
  );
}
export default TopBarComponent;
</code></pre><p>ContentComponent.js、BottomBarComponent.js代码一致，不再重复。</p><p>调整index.js和App.js代码</p><p>index.js</p><pre data-type="codeBlock" text="import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom/client&apos;;
import &apos;./index.css&apos;;
import App from &apos;./App&apos;;

const root = ReactDOM.createRoot(document.getElementById(&apos;root&apos;));
root.render(
  &amp;lt;App /&gt;
);
"><code><span class="hljs-keyword">import</span> <span class="hljs-title">React</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">ReactDOM</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react-dom/client'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./index.css'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">App</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./App'</span>;

const root <span class="hljs-operator">=</span> ReactDOM.createRoot(document.getElementById(<span class="hljs-string">'root'</span>));
root.render(
  <span class="hljs-operator">&#x26;</span>lt;App <span class="hljs-operator">/</span><span class="hljs-operator">></span>
);
</code></pre><p>App.js</p><pre data-type="codeBlock" text="import &apos;./App.css&apos;;
import BottomBarComponent from &apos;./bottombar/BottomBarComponent&apos;;
import ContentComponent from &apos;./content/ContentComponent&apos;;
import TopBarComponent from &apos;./topbar/TopBarComponent&apos;;

function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&gt;
      &amp;lt;TopBarComponent /&gt;
      &amp;lt;ContentComponent /&gt;
      &amp;lt;BottomBarComponent /&gt;
    &amp;lt;/div&gt;
  );
}

export default App;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./App.css'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">BottomBarComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./bottombar/BottomBarComponent'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">ContentComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./content/ContentComponent'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">TopBarComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./topbar/TopBarComponent'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">"App"</span><span class="hljs-operator">></span>
      <span class="hljs-operator">&#x26;</span>lt;TopBarComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>
      <span class="hljs-operator">&#x26;</span>lt;ContentComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>
      <span class="hljs-operator">&#x26;</span>lt;BottomBarComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>
    <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
  );
}

export default App;
</code></pre><p>此时我们的代码运行结果,已经将页面划分为3个区域(topbar因为和文章背景一致所以看不出来)</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/88e2754bbdab6bc581292ab60846548f9a3ab45f79ba16640132aedd2d6f4bc8.jpg" alt="545.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">545.jpeg</figcaption></figure><p>接下来，我们就去逐步完善每个区域。</p><h2 id="h-topbar" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">TopBar区域完善</h2><p>从最开始的图中可以看到TopBar可以分为3个区域:项目/社区名称，主功能tabs，连接钱包入口。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">项目/社区名称</h3><pre data-type="codeBlock" text="import &apos;./topbar.css&apos;;
const TitleComponent = () =&gt; {
    return (
        &amp;lt;div className=&apos;title&apos;&gt;
            &amp;lt;h1 className=&apos;title-text&apos; &gt; Web3探索者 &amp;lt;/h1&gt;
        &amp;lt;/div&gt;
    )
}
export default TitleComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./topbar.css'</span>;
const TitleComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'title'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h1 className<span class="hljs-operator">=</span><span class="hljs-string">'title-text'</span> <span class="hljs-operator">></span> Web3探索者 <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h1<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    )
}
export default TitleComponent;
</code></pre><h3 id="h-tab" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">主功能tab</h3><pre data-type="codeBlock" text="import &apos;./topbar.css&apos;;
const MainFeatureTabsComponent = ()=&gt; {
    return (
        &amp;lt;div className=&quot;tablist&quot;&gt;
            &amp;lt;div className=&quot;tablist-item&quot;&gt;
                &amp;lt;a href=&apos;/&apos; className=&apos;tablist-item-a&apos;&gt;简介&amp;lt;/a&gt;
            &amp;lt;/div&gt;
            &amp;lt;div className=&quot;tablist-item&quot; &gt;
                &amp;lt;a href=&apos;/mint&apos; className=&apos;tablist-item-a&apos;&gt; mint&amp;lt;/a&gt;
            &amp;lt;/div&gt;
            &amp;lt;div className=&quot;tablist-item&quot; &gt;
                &amp;lt;a href=&apos;/list&apos; className=&apos;tablist-item-a&apos;&gt;
                    NFT列表
                &amp;lt;/a&gt;
            &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
}

export default MainFeatureTabsComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./topbar.css'</span>;
const MainFeatureTabsComponent <span class="hljs-operator">=</span> ()<span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">"tablist"</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">"tablist-item"</span><span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;a href<span class="hljs-operator">=</span><span class="hljs-string">'/'</span> className<span class="hljs-operator">=</span><span class="hljs-string">'tablist-item-a'</span><span class="hljs-operator">></span>简介<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">"tablist-item"</span> <span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;a href<span class="hljs-operator">=</span><span class="hljs-string">'/mint'</span> className<span class="hljs-operator">=</span><span class="hljs-string">'tablist-item-a'</span><span class="hljs-operator">></span> mint<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">"tablist-item"</span> <span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;a href<span class="hljs-operator">=</span><span class="hljs-string">'/list'</span> className<span class="hljs-operator">=</span><span class="hljs-string">'tablist-item-a'</span><span class="hljs-operator">></span>
                    NFT列表
                <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
}

export default MainFeatureTabsComponent;
</code></pre><p>每次点击的时候切换子页面，后面Content页面的内容就会监听链接变化，发生变化则Content内部进行页面切换</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">钱包连接入口</h3><p>钱包连接入口这里的逻辑有一些复杂。我们需要实现下面的目标</p><ol><li><p>页面初始化的时候判断钱包是否已经连接，如果已连接展示其钱包地址。</p></li><li><p>中途用户连接钱包或断开钱包连接，需要更新对应状态，如果状态变为连接钱包需要展示钱包地址，如果状态变为未连接则展示连接钱包。</p></li><li><p>钱包未连接状态下按钮可点击，点击拉起钱包选择面板，连接状态下按钮不可点击。</p></li></ol><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">钱包状态读取</h4><p>钱包相关状态的读取，我们使用MetaMask提供的API，我们在src目录下新建utils目录，在utils下新建walletUtils.js封装和MetaMask钱包的交互。</p><pre data-type="codeBlock" text="//判断钱包是否安装
export const isMetaMaskInstalled = () =&gt; {
    const { ethereum } = window;
    return Boolean(ethereum &amp;&amp; ethereum.isMetaMask);
};

//请求连接钱包
export const metamaskConnect = async () =&gt; {
    try {
        const { ethereum } = window;
        return await ethereum.request({ method: &apos;eth_requestAccounts&apos; });
    } catch (error) {
        console.error(error);
    }
};
//获取当前已经连接的钱包地址
export const getConnectAccount = async () =&gt; {
    const { ethereum } = window;
    const account = await ethereum.request({ method: &apos;eth_accounts&apos; });
    console.log(&quot;getConnectAccount&quot; + account);
    return account;
}
"><code><span class="hljs-comment">//判断钱包是否安装</span>
export const isMetaMaskInstalled <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const { ethereum } <span class="hljs-operator">=</span> window;
    <span class="hljs-keyword">return</span> Boolean(ethereum <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> ethereum.isMetaMask);
};

<span class="hljs-comment">//请求连接钱包</span>
export const metamaskConnect <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-keyword">try</span> {
        const { ethereum } <span class="hljs-operator">=</span> window;
        <span class="hljs-keyword">return</span> await ethereum.request({ method: <span class="hljs-string">'eth_requestAccounts'</span> });
    } <span class="hljs-keyword">catch</span> (<span class="hljs-function"><span class="hljs-keyword">error</span>) </span>{
        console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
    }
};
<span class="hljs-comment">//获取当前已经连接的钱包地址</span>
export const getConnectAccount <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const { ethereum } <span class="hljs-operator">=</span> window;
    const account <span class="hljs-operator">=</span> await ethereum.request({ method: <span class="hljs-string">'eth_accounts'</span> });
    console.log(<span class="hljs-string">"getConnectAccount"</span> <span class="hljs-operator">+</span> account);
    <span class="hljs-keyword">return</span> account;
}
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">钱包状态管理</h4><p>由于钱包状态是1个全局状态，并非组件内状态，所以我们使用mobx来进行存储以及其状态变化的通知监听。</p><p>添加mobx依赖</p><pre data-type="codeBlock" text="npm i mobx
"><code>npm <span class="hljs-selector-tag">i</span> mobx
</code></pre><p>在src目录下新增Store.js来管理全局状态</p><pre data-type="codeBlock" text="import { observable } from &quot;mobx&quot;;
import { getConnectAccount } from &quot;./utils/wallet_utils&quot;;

const help = () =&gt; {
    // 钱包选择页面板是否展示状态
    const wallet_is_show = observable.box(false);
    // 当前连接的钱包地址，未连接则存储&quot;&quot;
    const currentAccount = observable.box(&quot;&quot;);
    //连接钱包地址发生变化时的处理，更新存储状态
    const handleAccountChange = (accounts) =&gt; {
        if (accounts.length === 0) {
            console.log(&quot;accounts is null&quot;);
            currentAccount.set(&quot;&quot;);
        } else if (accounts[0] !== currentAccount) {
            console.log(&quot;accounts is &quot; + accounts[0]);
            currentAccount.set(accounts[0]);
        }
    }

    //注册钱包状态的监听
    const registerAccountChange = () =&gt; {
        console.log(&quot;registerAccountChange&quot;);
        const { ethereum } = window;

        //注册时先获取一次当前状态
        getConnectAccount().
            then((accounts) =&gt; {
                handleAccountChange(accounts);
            }).catch((err) =&gt; {
                console.error(err);
            });

       //使用metamask提供的API注册钱包状态监听
        ethereum.on(&apos;accountsChanged&apos;, (accounts) =&gt; {
            handleAccountChange(accounts);
        });
    }

    return {wallet_is_show, registerAccountChange,currentAccount};

};
export const Store = help();
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">observable</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"mobx"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getConnectAccount</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./utils/wallet_utils"</span>;

const help <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-comment">// 钱包选择页面板是否展示状态</span>
    const wallet_is_show <span class="hljs-operator">=</span> observable.box(<span class="hljs-literal">false</span>);
    <span class="hljs-comment">// 当前连接的钱包地址，未连接则存储""</span>
    const currentAccount <span class="hljs-operator">=</span> observable.box(<span class="hljs-string">""</span>);
    <span class="hljs-comment">//连接钱包地址发生变化时的处理，更新存储状态</span>
    const handleAccountChange <span class="hljs-operator">=</span> (accounts) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        <span class="hljs-keyword">if</span> (accounts.<span class="hljs-built_in">length</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            console.log(<span class="hljs-string">"accounts is null"</span>);
            currentAccount.set(<span class="hljs-string">""</span>);
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (accounts[<span class="hljs-number">0</span>] <span class="hljs-operator">!</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> currentAccount) {
            console.log(<span class="hljs-string">"accounts is "</span> <span class="hljs-operator">+</span> accounts[<span class="hljs-number">0</span>]);
            currentAccount.set(accounts[<span class="hljs-number">0</span>]);
        }
    }

    <span class="hljs-comment">//注册钱包状态的监听</span>
    const registerAccountChange <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        console.log(<span class="hljs-string">"registerAccountChange"</span>);
        const { ethereum } <span class="hljs-operator">=</span> window;

        <span class="hljs-comment">//注册时先获取一次当前状态</span>
        getConnectAccount().
            then((accounts) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                handleAccountChange(accounts);
            }).catch((err) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                console.error(err);
            });

       <span class="hljs-comment">//使用metamask提供的API注册钱包状态监听</span>
        ethereum.on(<span class="hljs-string">'accountsChanged'</span>, (accounts) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
            handleAccountChange(accounts);
        });
    }

    <span class="hljs-keyword">return</span> {wallet_is_show, registerAccountChange,currentAccount};

};
export const Store <span class="hljs-operator">=</span> help();
</code></pre><p>在App.js中增加初始化监听钱包连接变化的调用</p><pre data-type="codeBlock" text="useEffect(() =&gt; {
    Store.registerAccountChange()
  }, []);
"><code><span class="hljs-built_in">useEffect</span>(() => {
    Store<span class="hljs-selector-class">.registerAccountChange</span>()
  }, <span class="hljs-selector-attr">[]</span>);
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">完成钱包入口组件</h4><p>完成钱包入口组件ToolbarComponent.js</p><pre data-type="codeBlock" text="import {Store} from &apos;../Store&apos;;
import &apos;./topbar.css&apos;;
import {useState } from &apos;react&apos;;

 const ToolbarComponent = () =&gt; {
     //组件内部定义入口按钮展示文案
    const [text, setText] = useState(Store.currentAccount.get() === &quot;&quot; ? &quot;连接钱包&quot; : Store.currentAccount.get());

    useEffect(() =&gt; {
      //使用mobx监听钱包状态的变化
        Store.currentAccount.observe_((newAccount) =&gt; {
            if (newAccount.newValue === &quot;&quot;) {
                setText(&quot;连接钱包&quot;);
            } else {
                setText(newAccount.newValue);
            }
        });   
    }, []);  

    return (
        &amp;lt;div className=&apos;toolbar&apos;&gt;
            &amp;lt;div className=&apos;connect-wallet&apos; &gt; 
                &amp;lt;h2 className=&apos;connect-wallet-text&apos; 
                    onClick={() =&gt; { onclick_wallet() }}&gt; {text} &amp;lt;/h2&gt;
             &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
}

const onclick_wallet = () =&gt; {
    if (Store.currentAccount.get() === &quot;&quot;) {
        Store.wallet_is_show.set(true);
    }
}
export default ToolbarComponent;
"><code><span class="hljs-keyword">import</span> {<span class="hljs-title">Store</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./topbar.css'</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;

 const ToolbarComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
     <span class="hljs-comment">//组件内部定义入口按钮展示文案</span>
    const [text, setText] <span class="hljs-operator">=</span> useState(Store.currentAccount.get() <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span> ? <span class="hljs-string">"连接钱包"</span> : Store.currentAccount.get());

    useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
      <span class="hljs-comment">//使用mobx监听钱包状态的变化</span>
        Store.currentAccount.observe_((newAccount) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
            <span class="hljs-keyword">if</span> (newAccount.newValue <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span>) {
                setText(<span class="hljs-string">"连接钱包"</span>);
            } <span class="hljs-keyword">else</span> {
                setText(newAccount.newValue);
            }
        });   
    }, []);  

    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'toolbar'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'connect-wallet'</span> <span class="hljs-operator">></span> 
                <span class="hljs-operator">&#x26;</span>lt;h2 className<span class="hljs-operator">=</span><span class="hljs-string">'connect-wallet-text'</span> 
                    onClick<span class="hljs-operator">=</span>{() <span class="hljs-operator">=</span><span class="hljs-operator">></span> { onclick_wallet() }}<span class="hljs-operator">></span> {text} <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h2<span class="hljs-operator">></span>
             <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
}

const onclick_wallet <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-keyword">if</span> (Store.currentAccount.get() <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span>) {
        Store.wallet_is_show.set(<span class="hljs-literal">true</span>);
    }
}
export default ToolbarComponent;
</code></pre><p>我们点击钱包入口的时候，如果未连接需要展示钱包选择页面。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">钱包选择页面</h4><p>在src目录下新建1个钱包页面目录wallet_page</p><p>我们钱包组件有以下能力</p><ol><li><p>打开钱包面板时判断是否已经安装MetaMask，如果未安装，则直接提示请先安装MetaMask</p></li><li><p>点击钱包面板时发起连接钱包请求</p></li><li><p>钱包连接成功，自动关闭</p></li><li><p>点击面板外其他位置时关闭面板</p></li></ol><p>在wallet_page下新建组件WalletComponent.js</p><pre data-type="codeBlock" text="import {Store} from &apos;../Store&apos;;
import &apos;./wallet-page.css&apos;;
import { useEffect, useState } from &apos;react&apos;;
import { isMetaMaskInstalled, metamaskConnect } from &apos;../utils/walletUtils&apos;;

const WalletComponent = () =&gt; {
    //组件内部定义钱包选择页面是否展示的状态。
    const [wallet_is_show, setWalletShow] = useState(false);
    useEffect(() =&gt; {
        //如果其他地方打开或关闭钱包选择页面，组件内部状态需要同步
        Store.wallet_is_show.observe_((change) =&gt; {
            setWalletShow(change.newValue);
        });
        // 如果账户状态发生改变，需要关闭该页面，比如连接钱包成功应该自动关闭该页面
        Store.currentAccount.observe_((change) =&gt; {
            Store.wallet_is_show.set(false);
        });
    }, []);

    if (!wallet_is_show) {
        return &amp;lt;div className=&apos;display-none&apos;&gt;&amp;lt;/div&gt;
    }

    const text = isMetaMaskInstalled() ? &quot;MetaMask&quot; : &quot;请先安装 MetaMask&quot;;
    return (
        &amp;lt;div className=&apos;wallet-page-bg&apos; onClick={cancel_click}&gt;
            &amp;lt;div className=&apos;wallet-page-panel&apos; onClick={(e) =&gt; {metaMask_click(e)}}&gt; {text} &amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
};

const cancel_click = () =&gt; {
    console.log(&quot;cancel_click&quot;);
    Store.wallet_is_show.set(false);
}

const metaMask_click = async (e) =&gt; {
    e.stopPropagation();
    if (isMetaMaskInstalled()) {
        metamaskConnect();
    }
    console.log(&quot;metaMask_click&quot;);
}

export default WalletComponent;
"><code><span class="hljs-keyword">import</span> {<span class="hljs-title">Store</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./wallet-page.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">useEffect</span>, <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">isMetaMaskInstalled</span>, <span class="hljs-title">metamaskConnect</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../utils/walletUtils'</span>;

const WalletComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-comment">//组件内部定义钱包选择页面是否展示的状态。</span>
    const [wallet_is_show, setWalletShow] <span class="hljs-operator">=</span> useState(<span class="hljs-literal">false</span>);
    useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        <span class="hljs-comment">//如果其他地方打开或关闭钱包选择页面，组件内部状态需要同步</span>
        Store.wallet_is_show.observe_((change) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
            setWalletShow(change.newValue);
        });
        <span class="hljs-comment">// 如果账户状态发生改变，需要关闭该页面，比如连接钱包成功应该自动关闭该页面</span>
        Store.currentAccount.observe_((change) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
            Store.wallet_is_show.set(<span class="hljs-literal">false</span>);
        });
    }, []);

    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>wallet_is_show) {
        <span class="hljs-keyword">return</span> <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'display-none'</span><span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    }

    const text <span class="hljs-operator">=</span> isMetaMaskInstalled() ? <span class="hljs-string">"MetaMask"</span> : <span class="hljs-string">"请先安装 MetaMask"</span>;
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'wallet-page-bg'</span> onClick<span class="hljs-operator">=</span>{cancel_click}<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'wallet-page-panel'</span> onClick<span class="hljs-operator">=</span>{(e) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {metaMask_click(e)}}<span class="hljs-operator">></span> {text} <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
};

const cancel_click <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    console.log(<span class="hljs-string">"cancel_click"</span>);
    Store.wallet_is_show.set(<span class="hljs-literal">false</span>);
}

const metaMask_click <span class="hljs-operator">=</span> async (e) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    e.stopPropagation();
    <span class="hljs-keyword">if</span> (isMetaMaskInstalled()) {
        metamaskConnect();
    }
    console.log(<span class="hljs-string">"metaMask_click"</span>);
}

export default WalletComponent;
</code></pre><p>在App.js中添加WalletComponent组件</p><p>注意，在测试钱包连接入口组件和钱包选择页面时，我们可以在metamask中断开已连接的网站，这样就可以进行反复测。</p><h2 id="h-bottombar" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">BottomBar区域完善</h2><pre data-type="codeBlock" text="import &apos;./bottombar.css&apos;;

const TWITTER_ICON = {图片链接}
const DISCORD_ICON = {图片链接}

function JoinComponent() {
    return (
        &amp;lt;div className=&apos;join&apos;&gt;
            &amp;lt;a href=&quot;https://twitter.com/twitter链接&quot; target=&quot;_blank&quot; className=&quot;join_item&quot;&gt;
            &amp;lt;img src={TWITTER_ICON} loading=&quot;lazy&quot; alt=&quot;&quot;/&gt;
            &amp;lt;/a&gt;
            &amp;lt;a href=&quot;https://discord.gg/discord链接&quot; target=&quot;_blank&quot; className=&quot;join_item&quot;&gt;
                &amp;lt;img src={DISCORD_ICON} loading=&quot;lazy&quot;/&gt;
            &amp;lt;/a&gt;
        &amp;lt;/div&gt;
    );
}
export default JoinComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./bottombar.css'</span>;

const TWITTER_ICON <span class="hljs-operator">=</span> {图片链接}
const DISCORD_ICON <span class="hljs-operator">=</span> {图片链接}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">JoinComponent</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'join'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;a href<span class="hljs-operator">=</span><span class="hljs-string">"https://twitter.com/twitter链接"</span> target<span class="hljs-operator">=</span><span class="hljs-string">"_blank"</span> className<span class="hljs-operator">=</span><span class="hljs-string">"join_item"</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;img src<span class="hljs-operator">=</span>{TWITTER_ICON} loading<span class="hljs-operator">=</span><span class="hljs-string">"lazy"</span> alt<span class="hljs-operator">=</span><span class="hljs-string">""</span><span class="hljs-operator">/</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;a href<span class="hljs-operator">=</span><span class="hljs-string">"https://discord.gg/discord链接"</span> target<span class="hljs-operator">=</span><span class="hljs-string">"_blank"</span> className<span class="hljs-operator">=</span><span class="hljs-string">"join_item"</span><span class="hljs-operator">></span>
                <span class="hljs-operator">&#x26;</span>lt;img src<span class="hljs-operator">=</span>{DISCORD_ICON} loading<span class="hljs-operator">=</span><span class="hljs-string">"lazy"</span><span class="hljs-operator">/</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
}
export default JoinComponent;
</code></pre><p>在BottomBarComponent.js中添加该组件</p><h2 id="h-content" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Content区域完善</h2><p>Content根据不同的tab将会展示3个页面:项目/社区简介页面, NFT展示页面, mint页面</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">项目/社区简介页面</h3><p>在content目录下新建MainPageComponent.js组件</p><pre data-type="codeBlock" text="import &apos;./content.css&apos;;
function MainPageComponent() {
    return (
        &amp;lt;div className=&apos;mainpage&apos;&gt;
            &amp;lt;h1 className=&apos;overview-title&apos;&gt;
                项目介绍
            &amp;lt;/h1&gt;
            &amp;lt;p className=&apos;overview-text&apos;&gt;
                白皮书内容
            &amp;lt;/p&gt;
        &amp;lt;/div&gt;

    );
}
export default MainPageComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./content.css'</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MainPageComponent</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'mainpage'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h1 className<span class="hljs-operator">=</span><span class="hljs-string">'overview-title'</span><span class="hljs-operator">></span>
                项目介绍
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h1<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;p className<span class="hljs-operator">=</span><span class="hljs-string">'overview-text'</span><span class="hljs-operator">></span>
                白皮书内容
            <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>p<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>

    );
}
export default MainPageComponent;
</code></pre><h3 id="h-nft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">NFT展示页面</h3><p>这个页面将是1个展示所有已经mint NFT的列表。我们这里只展示每1个NFT的图片以及名字。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">与合约进行交互</h4><p>NFT列表展示，需要从合约中读取一些数据，包括</p><ol><li><p>TokenID对应的TokenURI, 因为token的image name等信息都存储在TokenURI指向的metadata中</p></li><li><p>目前已经Mint过的所有tokenID，因为上篇文章我们的mint规则是顺序mint，所以我们只需要获得当前已经Mint的token总数，然后从0遍历即可。</p></li></ol><p>这里使用ethres.js与合约进行交互,添加ethres.js依赖</p><pre data-type="codeBlock" text="npm i ethers
"><code>npm <span class="hljs-selector-tag">i</span> ethers
</code></pre><p>另外需要注意，需要将MetaMask网络切换到rinkeby, 因为上篇文章我们是将合约部署在rinkeby测试网络中,只有在该网络从才可以获取到相应的合约信息。</p><p>根目录下新建abi目录将我们上篇文章中编译合约生成的NFT_WEB3_EXPOLRER.json 复制进去。</p><p>另外还有2点</p><ol><li><p>我们需要解析TokenURI对应的metadata,从中获取到名字、图片等具体数据</p></li><li><p>因为我们上篇文章中，metadata的image是使用Ipfs存储，浏览器不直接支持ipfs协议，所以我们需要将ipfs协议转换成为https协议，使用的工具库是我们上篇文章上传ipfs的平台https://app.pinata.cloud/提供。</p></li></ol><p>添加pinata/ipfs-gateway-tools依赖</p><pre data-type="codeBlock" text="npm i @pinata/ipfs-gateway-tools
"><code>npm i @pinata<span class="hljs-operator">/</span>ipfs<span class="hljs-operator">-</span>gateway<span class="hljs-operator">-</span>tools
</code></pre><p>在utils目录下新增contractUtils.js, 来实现一系列的封装方法达成上述功能。</p><pre data-type="codeBlock" text="// import Web3 from &quot;web3&quot;

import { ethers } from &apos;ethers&apos;;
import IPFSGatewayTools from &apos;@pinata/ipfs-gateway-tools/dist/browser&apos;;

const gatewayTools = new IPFSGatewayTools();

//获取tokenID对应的URI
export const getTokenURI = async (tokenID) =&gt; {
    const contract = getContract();
    return await contract.tokenURI(tokenID);
}

//获取合约
const getContract = () =&gt; {
    //在这里我们使用MetaMask向浏览器注入的provider
    const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
    const address = &quot;合约地址&quot;;
    const abi = require(&quot;../abi/NFT_WEB3_EXPOLRER.json&quot;).abi;
    return new ethers.Contract(address, abi, provider);
}

//获取已经mint的token总数
export const totalSupplyToken = async () =&gt; {
    const contract = getContract();
    return await contract.totalSupply();
}

//获取已mint tokenURI对应的metadata
export const getMetaDataList = async () =&gt; {
    const totalSupply = await totalSupplyToken();
    console.log(&quot;totalSupplyToken is&quot; + totalSupply);
    const list = []
    for (let tokenID = 0; tokenID &amp;lt; totalSupply; tokenID++) {
        const tokenURI = await getTokenURI(tokenID);
        const res = await parseMetaData(tokenURI);
        list.push(res);
    }
    return list;
}

//通过tokenURI获取metadata
export const parseMetaData = async (TokenURI) =&gt; {
    const res = await fetch(TokenURI);
    const json = await res.json();
    return json;
}

//将ipfs地址转为http地址
export const ipfsToHttp = (ipfsURL) =&gt; {
    return gatewayTools.convertToDesiredGateway(ipfsURL, &quot;https://gateway.pinata.cloud&quot;);
}
"><code><span class="hljs-comment">// import Web3 from "web3"</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">ethers</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'ethers'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">IPFSGatewayTools</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'@pinata/ipfs-gateway-tools/dist/browser'</span>;

const gatewayTools <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> IPFSGatewayTools();

<span class="hljs-comment">//获取tokenID对应的URI</span>
export const getTokenURI <span class="hljs-operator">=</span> async (tokenID) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title">getContract</span>(<span class="hljs-params"></span>);
    <span class="hljs-title"><span class="hljs-keyword">return</span></span> <span class="hljs-title">await</span> <span class="hljs-title"><span class="hljs-keyword">contract</span></span>.<span class="hljs-title">tokenURI</span>(<span class="hljs-params">tokenID</span>);
}

<span class="hljs-comment">//获取合约</span>
<span class="hljs-title">const</span> <span class="hljs-title">getContract</span> = (<span class="hljs-params"></span>) => </span>{
    <span class="hljs-comment">//在这里我们使用MetaMask向浏览器注入的provider</span>
    const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.Web3Provider(window.web3.currentProvider);
    const <span class="hljs-keyword">address</span> <span class="hljs-operator">=</span> <span class="hljs-string">"合约地址"</span>;
    const <span class="hljs-built_in">abi</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"../abi/NFT_WEB3_EXPOLRER.json"</span>).abi;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ethers.Contract(<span class="hljs-keyword">address</span>, <span class="hljs-built_in">abi</span>, provider);
}

<span class="hljs-comment">//获取已经mint的token总数</span>
export const totalSupplyToken <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title">getContract</span>(<span class="hljs-params"></span>);
    <span class="hljs-title"><span class="hljs-keyword">return</span></span> <span class="hljs-title">await</span> <span class="hljs-title"><span class="hljs-keyword">contract</span></span>.<span class="hljs-title">totalSupply</span>(<span class="hljs-params"></span>);
}

<span class="hljs-comment">//获取已mint tokenURI对应的metadata</span>
<span class="hljs-title">export</span> <span class="hljs-title">const</span> <span class="hljs-title">getMetaDataList</span> = <span class="hljs-title">async</span> (<span class="hljs-params"></span>) => </span>{
    const totalSupply <span class="hljs-operator">=</span> await totalSupplyToken();
    console.log(<span class="hljs-string">"totalSupplyToken is"</span> <span class="hljs-operator">+</span> totalSupply);
    const list <span class="hljs-operator">=</span> []
    <span class="hljs-keyword">for</span> (let tokenID <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; tokenID <span class="hljs-operator">&#x26;</span>lt; totalSupply; tokenID<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        const tokenURI <span class="hljs-operator">=</span> await getTokenURI(tokenID);
        const res <span class="hljs-operator">=</span> await parseMetaData(tokenURI);
        list.<span class="hljs-built_in">push</span>(res);
    }
    <span class="hljs-keyword">return</span> list;
}

<span class="hljs-comment">//通过tokenURI获取metadata</span>
export const parseMetaData <span class="hljs-operator">=</span> async (TokenURI) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const res <span class="hljs-operator">=</span> await fetch(TokenURI);
    const json <span class="hljs-operator">=</span> await res.json();
    <span class="hljs-keyword">return</span> json;
}

<span class="hljs-comment">//将ipfs地址转为http地址</span>
export const ipfsToHttp <span class="hljs-operator">=</span> (ipfsURL) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    <span class="hljs-keyword">return</span> gatewayTools.convertToDesiredGateway(ipfsURL, <span class="hljs-string">"https://gateway.pinata.cloud"</span>);
}
</code></pre><p>接下来在content目录下新建NFTListComponent组件调用上述方法进行展示</p><pre data-type="codeBlock" text="import &apos;./content.css&apos;;
import { getMetaDataList, ipfsToHttp } from &quot;../utils/contractUtils&quot;;
import { useEffect, useState } from &apos;react&apos;;

function NFTListComponent() {
    const [metadatalist, setMetadataList] = useState([]);

    useEffect(() =&gt; {
        //获取NFT的MetaMata列表
        getMetaDataList()
            .then((arr) =&gt; {
                setMetadataList(arr);
            })
            .catch((err) =&gt; {
                console.error(&quot;err is&quot; + err);
            });
    }, []);

    const items = metadatalist.map((metadata, index) =&gt; 
        &amp;lt;div className=&apos;nft-list-item&apos; key={index}&gt;
            &amp;lt;img className=&apos;nft-list-item-img&apos;  src={ipfsToHttp(metadata.image)} /&gt;
            &amp;lt;h4 className=&apos;nft-list-item-name&apos;&gt; {metadata.name} &amp;lt;/h4&gt;
        &amp;lt;/div&gt;
    );
    console.log(&quot;items is&quot; + items);

    return (
        &amp;lt;div className=&apos;nft-list&apos;&gt;
            {items}
        &amp;lt;/div&gt;

    );
}
export default NFTListComponent;
"><code><span class="hljs-keyword">import</span> <span class="hljs-string">'./content.css'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getMetaDataList</span>, <span class="hljs-title">ipfsToHttp</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"../utils/contractUtils"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">useEffect</span>, <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NFTListComponent</span>(<span class="hljs-params"></span>) </span>{
    const [metadatalist, setMetadataList] <span class="hljs-operator">=</span> useState([]);

    useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        <span class="hljs-comment">//获取NFT的MetaMata列表</span>
        getMetaDataList()
            .then((arr) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                setMetadataList(arr);
            })
            .catch((err) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                console.error(<span class="hljs-string">"err is"</span> <span class="hljs-operator">+</span> err);
            });
    }, []);

    const items <span class="hljs-operator">=</span> metadatalist.map((metadata, index) <span class="hljs-operator">=</span><span class="hljs-operator">></span> 
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'nft-list-item'</span> key<span class="hljs-operator">=</span>{index}<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;img className<span class="hljs-operator">=</span><span class="hljs-string">'nft-list-item-img'</span>  src<span class="hljs-operator">=</span>{ipfsToHttp(metadata.image)} <span class="hljs-operator">/</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;h4 className<span class="hljs-operator">=</span><span class="hljs-string">'nft-list-item-name'</span><span class="hljs-operator">></span> {metadata.<span class="hljs-built_in">name</span>} <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>h4<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
    console.log(<span class="hljs-string">"items is"</span> <span class="hljs-operator">+</span> items);

    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'nft-list'</span><span class="hljs-operator">></span>
            {items}
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>

    );
}
export default NFTListComponent;
</code></pre><p>接下来我们来实现最后1个页面NFT的mint页面</p><h3 id="h-nft-mint" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">NFT mint页面</h3><p>这个页面有2个功能</p><ol><li><p>展示mint进度</p></li><li><p>提供mint入口</p></li></ol><p>为了实现这2个功能，我们需要增加一些与合约交互的能力。在contractUtils.js中新增3个函数</p><pre data-type="codeBlock" text="//获取最大可mint数量
export const maxSupplyToken = async () =&gt; {
    const contract = getContract();
    return await contract.MAX_SUPPLY();
}

//同时返回当前mint总数和最大mint总数
export const getMintInfo = async () =&gt; {
    const totalSupply = await totalSupplyToken();
    const maxSupply = await maxSupplyToken();
    return [totalSupply, maxSupply];
}

// mint功能，这里同样是使用metamask进行签名，调用该方法MetaMask会弹出交易确认按钮
export const mint = async () =&gt; {
    const contract = getContract();
    const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
    const signer = provider.getSigner();
    const contractWithSigner = contract.connect(signer);
    const price = await contract.PRICE_PER_TOKEN();
    console.log(&quot;price is&quot; + price);
    const tx = await contractWithSigner.mint(1, { value: price });
    console.log(tx.hash);
    await tx.wait();
}
"><code>//获取最大可mint数量
export const <span class="hljs-attr">maxSupplyToken</span> = async () => {
    const <span class="hljs-attr">contract</span> = getContract()<span class="hljs-comment">;</span>
    return await contract.MAX_SUPPLY()<span class="hljs-comment">;</span>
}

//同时返回当前mint总数和最大mint总数
export const <span class="hljs-attr">getMintInfo</span> = async () => {
    const <span class="hljs-attr">totalSupply</span> = await totalSupplyToken()<span class="hljs-comment">;</span>
    const <span class="hljs-attr">maxSupply</span> = await maxSupplyToken()<span class="hljs-comment">;</span>
    return <span class="hljs-section">[totalSupply, maxSupply]</span><span class="hljs-comment">;</span>
}

// mint功能，这里同样是使用metamask进行签名，调用该方法MetaMask会弹出交易确认按钮
export const <span class="hljs-attr">mint</span> = async () => {
    const <span class="hljs-attr">contract</span> = getContract()<span class="hljs-comment">;</span>
    const <span class="hljs-attr">provider</span> = new ethers.providers.Web3Provider(window.web3.currentProvider)<span class="hljs-comment">;</span>
    const <span class="hljs-attr">signer</span> = provider.getSigner()<span class="hljs-comment">;</span>
    const <span class="hljs-attr">contractWithSigner</span> = contract.connect(signer)<span class="hljs-comment">;</span>
    const <span class="hljs-attr">price</span> = await contract.PRICE_PER_TOKEN()<span class="hljs-comment">;</span>
    console.log("price is" + price)<span class="hljs-comment">;</span>
    const <span class="hljs-attr">tx</span> = await contractWithSigner.mint(<span class="hljs-number">1</span>, { value: price })<span class="hljs-comment">;</span>
    console.log(tx.hash)<span class="hljs-comment">;</span>
    await tx.wait()<span class="hljs-comment">;</span>
}
</code></pre><p>实现MintPageComponent组件</p><pre data-type="codeBlock" text="import { useEffect, useState } from &apos;react&apos;;
import { Store } from &apos;../Store&apos;;
import { getMintInfo, mint } from &apos;../utils/contractUtils&apos;;
import &apos;./content.css&apos;;

const MintPageComponent = () =&gt; {
    const [totalSupply, setTotalSupply] = useState(0);
    const [maxSupply, setMaxTotalSupply] = useState(0);

    useEffect(() =&gt; {
        updataMintInfo();
    }, []);

    const updataMintInfo = () =&gt; {
        getMintInfo()
            .then((info) =&gt; {
                setTotalSupply(info[0]);
                setMaxTotalSupply(info[1]);
            })
            .catch((err) =&gt; {
                console.error(err);
            });
    }

    const mintClick = () =&gt; {
        if (Store.currentAccount.get() === &quot;&quot;) {
            window.alert(&quot;请先连接钱包&quot;);
            return;
        }
        mint()
        .then(() =&gt; {
            //mint成功后更新mint信息
            updataMintInfo();
            window.alert(&quot;mint成功&quot;);
        }).catch();
    }

    return (
        &amp;lt;div className=&apos;mintpage&apos;&gt;
            &amp;lt;div className=&apos;mint-info&apos;&gt;当前mint进度 {&quot;&quot; + totalSupply} / {&quot;&quot; + maxSupply}&amp;lt;/div&gt;
            &amp;lt;div className=&apos;mint-btn&apos; onClick={mintClick}&gt;一键mint&amp;lt;/div&gt;
        &amp;lt;/div&gt;
    );
}

export default MintPageComponent;
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">useEffect</span>, <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Store</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../Store'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getMintInfo</span>, <span class="hljs-title">mint</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'../utils/contractUtils'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./content.css'</span>;

const MintPageComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    const [totalSupply, setTotalSupply] <span class="hljs-operator">=</span> useState(<span class="hljs-number">0</span>);
    const [maxSupply, setMaxTotalSupply] <span class="hljs-operator">=</span> useState(<span class="hljs-number">0</span>);

    useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        updataMintInfo();
    }, []);

    const updataMintInfo <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        getMintInfo()
            .then((info) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                setTotalSupply(info[<span class="hljs-number">0</span>]);
                setMaxTotalSupply(info[<span class="hljs-number">1</span>]);
            })
            .catch((err) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
                console.error(err);
            });
    }

    const mintClick <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
        <span class="hljs-keyword">if</span> (Store.currentAccount.get() <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span>) {
            window.alert(<span class="hljs-string">"请先连接钱包"</span>);
            <span class="hljs-keyword">return</span>;
        }
        mint()
        .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
            <span class="hljs-comment">//mint成功后更新mint信息</span>
            updataMintInfo();
            window.alert(<span class="hljs-string">"mint成功"</span>);
        }).catch();
    }

    <span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'mintpage'</span><span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'mint-info'</span><span class="hljs-operator">></span>当前mint进度 {<span class="hljs-string">""</span> <span class="hljs-operator">+</span> totalSupply} <span class="hljs-operator">/</span> {<span class="hljs-string">""</span> <span class="hljs-operator">+</span> maxSupply}<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
            <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'mint-btn'</span> onClick<span class="hljs-operator">=</span>{mintClick}<span class="hljs-operator">></span>一键mint<span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
        <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
    );
}

export default MintPageComponent;
</code></pre><p>最后我们在ContentComponent增加MainPageComponent、MintPageComponent、NFTListComponent，并设置路由状态变化的监听</p><pre data-type="codeBlock" text="import { useEffect, useState } from &apos;react&apos;;
import &apos;./content.css&apos;;
import MainPageComponent from &apos;./MainPageComponent&apos;;
import MintPageComponent from &apos;./MintPageComponent&apos;;
import NFTListComponent from &apos;./NFTListComponent&apos;;

const ContentComponent = () =&gt; {
  const [route, setRoute] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    setRoute(window.location.pathname);
  });

  return (
    &amp;lt;div className=&apos;content&apos;&gt;
      {(route != &quot;/mint&quot; &amp;&amp; route != &quot;/list&quot;) &amp;&amp; &amp;lt;MainPageComponent /&gt;}
      {(route == &quot;/mint&quot;) &amp;&amp; &amp;lt;MintPageComponent /&gt;}
      {(route == &quot;/list&quot;) &amp;&amp; &amp;lt;NFTListComponent /&gt;}
    &amp;lt;/div&gt;
  );
}

export default ContentComponent;
"><code><span class="hljs-keyword">import</span> { <span class="hljs-title">useEffect</span>, <span class="hljs-title">useState</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./content.css'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">MainPageComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./MainPageComponent'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">MintPageComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./MintPageComponent'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">NFTListComponent</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./NFTListComponent'</span>;

const ContentComponent <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
  const [route, setRoute] <span class="hljs-operator">=</span> useState(<span class="hljs-string">""</span>);

  useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
    setRoute(window.location.pathname);
  });

  <span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&#x26;</span>lt;div className<span class="hljs-operator">=</span><span class="hljs-string">'content'</span><span class="hljs-operator">></span>
      {(route <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-string">"/mint"</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> route <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-string">"/list"</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> <span class="hljs-operator">&#x26;</span>lt;MainPageComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>}
      {(route <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">"/mint"</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> <span class="hljs-operator">&#x26;</span>lt;MintPageComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>}
      {(route <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">"/list"</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> <span class="hljs-operator">&#x26;</span>lt;NFTListComponent <span class="hljs-operator">/</span><span class="hljs-operator">></span>}
    <span class="hljs-operator">&#x26;</span>lt;<span class="hljs-operator">/</span>div<span class="hljs-operator">></span>
  );
}

export default ContentComponent;
</code></pre><p>好了，目前为止我们的NFT官网就搭建完成了。基本的功能都已经实现，页面框架也有了。当然如果要进行上线官网的UI还需要进行仔细设计。后面还可以增加查看归属自己的NFT等功能，同时比如mint进度这里我们没有监听其他人在我们浏览页面过程中的mint事件，可能导致更新不及时，篇幅原因这里不再介绍。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>我们通过2篇文章来介绍NFT的发布，第一篇重点讲解如何上链，可以归属于后端逻辑。第二篇讲解官方搭建，可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例，希望能够帮助到希望进入web3行业的同学。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/e411910e2d2b036509077b9d26277d1232b856fa298eab521e427b4335e7c8fe.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[如何发行一款NFT(上)]]></title>
            <link>https://paragraph.com/@explorer-of-web3/nft</link>
            <guid>z7RgvUbgn0HBarswH9Hv</guid>
            <pubDate>Mon, 02 May 2022 06:22:01 GMT</pubDate>
            <description><![CDATA[希望通过本文，可以让有发行NFT想法的小伙伴，发行一款自己的NFT。 如何发行一款社区概念的NFT，一般是有4个步骤发起人完成白皮书完成NFT设计并编写智能合约代码发布到链上完成NFT社区官网，一般包含白皮书简介，mint,NFT列表展览等功能社区运营以及持续为达成白皮书的目标而合作迭代我们选择技术点涉及较多的2、3步骤和大家讨论。分为上下两篇文章，本文将重点讲解第2点如何完成NFT智能合约的开发、部署、交互以及在交易市场查看自己发行的NFT。下篇文章会重点讲解第3点NFT社区官网的搭建。合约开发环境选择目前主流的智能合约开发环境有3个:remix,hardhat, truffle。remix是浏览器IDE，虽然开箱即用，上手简单，但在灵活性和功能完善度上不如hardhat, truffle。hardhat, truffle相比较，truffle是老牌主流框架，hardhat是新起之秀，但目前也足够稳定。 我们最终选择hardhat作为我们的本地合约开发环境。初始化工程//创建一款web3探索者的nft mkdir nft-web3-explorer //进入目录 cd nft-...]]></description>
            <content:encoded><![CDATA[<p>希望通过本文，可以让有发行NFT想法的小伙伴，发行一款自己的NFT。</p><p>如何发行一款社区概念的NFT，一般是有4个步骤</p><ol><li><p>发起人完成白皮书</p></li><li><p>完成NFT设计并编写智能合约代码发布到链上</p></li><li><p>完成NFT社区官网，一般包含白皮书简介，mint,NFT列表展览等功能</p></li><li><p>社区运营以及持续为达成白皮书的目标而合作迭代</p></li></ol><p>我们选择技术点涉及较多的2、3步骤和大家讨论。分为上下两篇文章，本文将重点讲解第2点如何完成NFT智能合约的开发、部署、交互以及在交易市场查看自己发行的NFT。下篇文章会重点讲解第3点NFT社区官网的搭建。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约开发环境选择</h3><p>目前主流的智能合约开发环境有3个:remix,hardhat, truffle。remix是浏览器IDE，虽然开箱即用，上手简单，但在灵活性和功能完善度上不如hardhat, truffle。hardhat, truffle相比较，truffle是老牌主流框架，hardhat是新起之秀，但目前也足够稳定。 我们最终选择hardhat作为我们的本地合约开发环境。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">初始化工程</h3><pre data-type="codeBlock" text="//创建一款web3探索者的nft
mkdir  nft-web3-explorer

//进入目录
cd nft-web3-explorer

//初始化项目，根据提示填写即可，packname和description填写即可
npm init

//添加hardhat依赖
npm install --save-dev hardhat


/*使用脚手架搭建项目,我们选择Create a basic sample project
可以帮助我们创建1个demo工程并按照所需的依赖*/
npx hardhat
"><code><span class="hljs-comment">//创建一款web3探索者的nft</span>
mkdir  nft<span class="hljs-operator">-</span>web3<span class="hljs-operator">-</span>explorer

<span class="hljs-comment">//进入目录</span>
cd nft<span class="hljs-operator">-</span>web3<span class="hljs-operator">-</span>explorer

<span class="hljs-comment">//初始化项目，根据提示填写即可，packname和description填写即可</span>
npm init

<span class="hljs-comment">//添加hardhat依赖</span>
npm install <span class="hljs-operator">-</span><span class="hljs-operator">-</span>save<span class="hljs-operator">-</span>dev hardhat


<span class="hljs-comment">/*使用脚手架搭建项目,我们选择Create a basic sample project
可以帮助我们创建1个demo工程并按照所需的依赖*/</span>
npx hardhat
</code></pre><p>创建完成后，我们看看我们的目录结构</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6ebb4d31707de45d74fe7a7a3246c96edb42d996e8926d1728f0792274c46bf9.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><ul><li><p>contracts目录用来存放我们的智能合约代码</p></li><li><p>scripts用来存放我们的脚本，比如合约部署就会依赖其中的脚本</p></li><li><p>test目录用来存放我们为智能合约编写的测试代码</p></li><li><p>hardhat.config.js是关于hardhat框架的一些配置，比如solidity版本等</p></li><li><p>package.json是npm的相关配置</p></li></ul><p>使用vscode打开该工程,最好安装vscode的solidity插件。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">编写合约代码</h3><p>目前就可以开始写合约代码了，本文主要是介绍NFT的发行，我们简单来实现一份NFT智能合约代码。</p><pre data-type="codeBlock" text="//安装openzeppelin/contracts依赖，内置较多合约协议的实现以及工具代码
npm install @openzeppelin/contracts
"><code>//安装openzeppelin/contracts依赖，内置较多合约协议的实现以及工具代码
npm install @openzeppelin/contracts
</code></pre><p>我们在contracts目录下创建NFT_WEB3_EXPOLRER.sol的文件。</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;

import &quot;@openzeppelin/contracts/token/ERC721/ERC721.sol&quot;;
import &quot;@openzeppelin/contracts/access/Ownable.sol&quot;;
import &quot;@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol&quot;;

contract NFT_WEB3_EXPLORER is ERC721, ERC721Enumerable, Ownable {
    string private _baseURIextended;
    //我们设置最多可以mint1000个
    uint256 public constant MAX_SUPPLY = 1000;
    //每个mint的价格是0.01EHT
    uint256 public constant PRICE_PER_TOKEN = 0.01 ether;

    constructor() ERC721(&quot;nft_web3_explorer&quot;, &quot;NFT_WEB3_EXPLORER&quot;) {
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function setBaseURI(string memory baseURI_) external onlyOwner() {
        _baseURIextended = baseURI_;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseURIextended;
    }

    function mint(uint numberOfTokens) public payable {
        uint256 ts = totalSupply();
        require(ts + numberOfTokens &lt;= MAX_SUPPLY, &quot;Purchase would exceed max tokens&quot;);
        require(PRICE_PER_TOKEN * numberOfTokens &lt;= msg.value, &quot;Ether value sent is not correct&quot;);

        for (uint256 i = 0; i &lt; numberOfTokens; i++) {
            _safeMint(msg.sender, ts + i);
        }
    }

    function withdraw() public onlyOwner {
        uint balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/token/ERC721/ERC721.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/access/Ownable.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">NFT_WEB3_EXPLORER</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC721</span>, <span class="hljs-title">ERC721Enumerable</span>, <span class="hljs-title">Ownable</span> </span>{
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">private</span> _baseURIextended;
    <span class="hljs-comment">//我们设置最多可以mint1000个</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> MAX_SUPPLY <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;
    <span class="hljs-comment">//每个mint的价格是0.01EHT</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> PRICE_PER_TOKEN <span class="hljs-operator">=</span> <span class="hljs-number">0</span><span class="hljs-number">.01</span> <span class="hljs-literal">ether</span>;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC721</span>(<span class="hljs-params"><span class="hljs-string">"nft_web3_explorer"</span>, <span class="hljs-string">"NFT_WEB3_EXPLORER"</span></span>) </span>{
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_beforeTokenTransfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>, <span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span>(<span class="hljs-params">ERC721, ERC721Enumerable</span>) </span>{
        <span class="hljs-built_in">super</span>._beforeTokenTransfer(<span class="hljs-keyword">from</span>, to, tokenId);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">supportsInterface</span>(<span class="hljs-params"><span class="hljs-keyword">bytes4</span> interfaceId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span>(<span class="hljs-params">ERC721, ERC721Enumerable</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">super</span>.supportsInterface(interfaceId);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setBaseURI</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> baseURI_</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span>(<span class="hljs-params"></span>) </span>{
        _baseURIextended <span class="hljs-operator">=</span> baseURI_;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_baseURI</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-keyword">return</span> _baseURIextended;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> numberOfTokens</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        <span class="hljs-keyword">uint256</span> ts <span class="hljs-operator">=</span> totalSupply();
        <span class="hljs-built_in">require</span>(ts <span class="hljs-operator">+</span> numberOfTokens <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> MAX_SUPPLY, <span class="hljs-string">"Purchase would exceed max tokens"</span>);
        <span class="hljs-built_in">require</span>(PRICE_PER_TOKEN <span class="hljs-operator">*</span> numberOfTokens <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>, <span class="hljs-string">"Ether value sent is not correct"</span>);

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> numberOfTokens; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            _safeMint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, ts <span class="hljs-operator">+</span> i);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-keyword">uint</span> balance <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
        <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">transfer</span>(balance);
    }
}
</code></pre><p>非常简单，这源于ERC721协议的帮助。该代码不具备上线标准，因为没有白名单，每个地址限制mint多少个等，仅供示意。建议发行NFT前需要好好设计合约，</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约编译</h3><pre data-type="codeBlock" text="//合约编译
npx hardhat compile
"><code><span class="hljs-comment">//合约编译</span>
npx hardhat compile
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约部署</h3><p>修改scripts目录下自动生成的脚本改名为deploy.js并修改内部代码</p><pre data-type="codeBlock" text="const hre = require(&quot;hardhat&quot;);

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run(&apos;compile&apos;);

  // We get the contract to deploy
  const Nft_web3_explrer = await hre.ethers.getContractFactory(&quot;NFT_WEB3_EXPLORER&quot;);
  const nft_web3_explrer = await Nft_web3_explrer.deploy();

  await nft_web3_explrer.deployed();

  console.log(&quot;NFT_WEB3_EXPLORER deployed to:&quot;, nft_web3_explorer.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() =&gt; process.exit(0))
  .catch((error) =&gt; {
    console.error(error);
    process.exit(1);
  });
"><code>const hre <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Hardhat always runs the compile task when running scripts with its command</span>
  <span class="hljs-comment">// line interface.</span>
  <span class="hljs-comment">//</span>
  <span class="hljs-comment">// If this script is run directly using `node` you may want to call compile</span>
  <span class="hljs-comment">// manually to make sure everything is compiled</span>
  <span class="hljs-comment">// await hre.run('compile');</span>

  <span class="hljs-comment">// We get the contract to deploy</span>
  const Nft_web3_explrer <span class="hljs-operator">=</span> await hre.ethers.getContractFactory(<span class="hljs-string">"NFT_WEB3_EXPLORER"</span>);
  const nft_web3_explrer <span class="hljs-operator">=</span> await Nft_web3_explrer.deploy();

  await nft_web3_explrer.deployed();

  console.log(<span class="hljs-string">"NFT_WEB3_EXPLORER deployed to:"</span>, nft_web3_explorer.<span class="hljs-built_in">address</span>);
}

<span class="hljs-comment">// We recommend this pattern to be able to use async/await everywhere</span>
<span class="hljs-comment">// and properly handle errors.</span>
main()
  .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
  .catch((<span class="hljs-function"><span class="hljs-keyword">error</span>) => </span>{
    console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
    process.exit(<span class="hljs-number">1</span>);
  });
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">选择节点服务代理</h4><p>此时，因为我们并不想在本地运行1个以太坊节点，所以我们使用节点服务代理(管理节点)，我们与他们进行api交互，他们会在云上管理的节点中执行对应的节点操作。这里我们选用与MetaMask同一服务代理 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://infura.io/">https://infura.io/</a> 注册账号后，我们创建1个项目</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f561bf62fdc1a7aeff6f1b1d7327fae98becc5a5a2a33b36bf0cbba43b0ca6ba.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">测试网络环境搭建</h4><p>我们首先需要在测试网络中部署，由于很多NFT交易市场只支持rinkeby，所以我们选择rinkeby作为我们的测试网络。</p><p>因为在代码执行过程中会用到公钥、私钥、API等数据，但这些数据又不能编码到代码中，因为如果我们使用git来管理项目，信息将会泄露。我们使用dotenv存放部署合约以及和合约交互需要用到的数据。</p><pre data-type="codeBlock" text="//添加dotenv依赖
npm install dotenv

//工程根目录下创建变量存储文件
touch .env
"><code><span class="hljs-comment">//添加dotenv依赖</span>
npm install dotenv

<span class="hljs-comment">//工程根目录下创建变量存储文件</span>
touch .env
</code></pre><p>为.env增加内容</p><pre data-type="codeBlock" text="PRIVATE_KEY=导出你metamask的私钥填在这里
API=找到节点服务代理URL填在这里
PUBLIC_KEY=metamask地址
NETWORK=rinkeby
API_KEY=节点服务代理的ProjectID
"><code><span class="hljs-attr">PRIVATE_KEY</span>=导出你metamask的私钥填在这里
<span class="hljs-attr">API</span>=找到节点服务代理URL填在这里
<span class="hljs-attr">PUBLIC_KEY</span>=metamask地址
<span class="hljs-attr">NETWORK</span>=rinkeby
<span class="hljs-attr">API_KEY</span>=节点服务代理的ProjectID
</code></pre><ul><li><p>metamask私钥导出:打开钱包-账号详情-&gt;导出私钥。</p></li><li><p>服务代理URL:将网络切换到ROPSTEN，复制下面的https链接。</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ceaae2eae36ceacc39ee11cbcbdf942c8352983978ca09f298c8716848a98fee.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>修改hardhat.config.js</p><pre data-type="codeBlock" text="require(&quot;@nomiclabs/hardhat-waffle&quot;);

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
require(&apos;dotenv&apos;).config();
const { API, PRIVATE_KEY } = process.env;

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import(&apos;hardhat/config&apos;).HardhatUserConfig
 */
module.exports = {
  solidity: &quot;0.8.4&quot;,
  defaultNetwork: &quot;rinkeby&quot;,
  networks: {
    hardhat: {},
    rinkeby: {
      url: API,
      accounts: [`0x${PRIVATE_KEY}`]
    }
  },
};
"><code><span class="hljs-built_in">require</span>(<span class="hljs-string">"@nomiclabs/hardhat-waffle"</span>);

<span class="hljs-comment">// This is a sample Hardhat task. To learn how to create your own go to</span>
<span class="hljs-comment">// https://hardhat.org/guides/create-task.html</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config();
const { API, PRIVATE_KEY } <span class="hljs-operator">=</span> process.env;

<span class="hljs-comment">// You need to export an object to set up your config</span>
<span class="hljs-comment">// Go to https://hardhat.org/config/ to learn more</span>

<span class="hljs-comment">/**
 * @type import('hardhat/config').HardhatUserConfig
 */</span>
module.exports <span class="hljs-operator">=</span> {
  solidity: <span class="hljs-string">"0.8.4"</span>,
  defaultNetwork: <span class="hljs-string">"rinkeby"</span>,
  networks: {
    hardhat: {},
    rinkeby: {
      url: API,
      accounts: [`0x${PRIVATE_KEY}`]
    }
  },
};
</code></pre><p>获取测试网络ETH 因为合约测试需要消耗ETH，我们需要获取一些测试完了的ETH，我们打开<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://fauceth.komputing.org/">https://fauceth.komputing.org/</a> 选择rinkeby网络，填入我们的钱包地址即可获取。</p><p>此时我们将钱包切换到rinkeby测试网络，可以看到钱包中已经有一些ETH了。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">执行合约部署</h4><pre data-type="codeBlock" text="npx hardhat --network rinkeby run scripts/deploy.js

//执行完成后得到提示，代表合约部署完成
NFT_WEB3_EXPOLRER deployed to: {合约地址}
"><code>npx hardhat <span class="hljs-operator">-</span><span class="hljs-operator">-</span>network rinkeby run scripts<span class="hljs-operator">/</span>deploy.js

<span class="hljs-comment">//执行完成后得到提示，代表合约部署完成</span>
NFT_WEB3_EXPOLRER deployed to: {合约地址}
</code></pre><p>此时我们可以在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://rinkeby.etherscan.io/">https://rinkeby.etherscan.io/</a>搜索我们的合约地址找到我们的合约信息</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/203c748b6ef33756fadfcec1a89c4397b38c7fa09febc0027a96dd9ef5d6ad97.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-nft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">为NFT设置资源</h3><p>现在的NFT仅仅包含tokenID，我们需要为其设置资源, 在ERC721的实现中我们可以看到tokenURI方法的实现，各个交易市场就是调用该方法来读取NFT资源信息的</p><pre data-type="codeBlock" text="    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), &quot;ERC721Metadata: URI query for nonexistent token&quot;);

        string memory baseURI = _baseURI();
        return bytes(baseURI).length &gt; 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : &quot;&quot;;
    }
"><code>    <span class="hljs-comment">/**
     * @dev See {IERC721Metadata-tokenURI}.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">tokenURI</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-built_in">require</span>(_exists(tokenId), <span class="hljs-string">"ERC721Metadata: URI query for nonexistent token"</span>);

        <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> baseURI <span class="hljs-operator">=</span> _baseURI();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">bytes</span>(baseURI).<span class="hljs-built_in">length</span> <span class="hljs-operator">></span> <span class="hljs-number">0</span> ? <span class="hljs-keyword">string</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(baseURI, tokenId.toString())) : <span class="hljs-string">""</span>;
    }
</code></pre><p>我们可以看到这里是将BaseURI和tokenID拼接来读取tokenURI的，所以我们也按照这种格式来准备资源。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">准备资源</h4><pre data-type="codeBlock" text="//创建资源文件
mkdir res
cd res
//存放图片
mkdir  img
//存放metadata信息
mkdir  metadata
"><code><span class="hljs-comment">//创建资源文件</span>
mkdir res
cd res
<span class="hljs-comment">//存放图片</span>
mkdir  img
<span class="hljs-comment">//存放metadata信息</span>
mkdir  metadata
</code></pre><p>由于本文是示例项目，没有找专门的设计师设计图片，我们网上找了7张图片。将7张图片放入img目录下，依次命名为0.png ~ 7.png</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d0f0c4901e38e74059cea382ee3802cecd096afeeaea871dc56c8a39c4a3d402.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>我们将7张图片上传到ipfs中，这里和以太坊网络一样，由于我们不想运行本地ipfs节点，所以我们寻找节点服务提供商进行上传。我们使用<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://app.pinata.cloud/">https://app.pinata.cloud/</a>选择上传目录进行将img目录上传 上传完成后的图片资源</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2fed12411762963795e2cdeceb690e3e36d506e7b286cc6a21e93eef6381c07a.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>接下来我们准备metadata文件,在metadata目录下新建7个文件依次命名为0~7，我们看下0号文件信息</p><pre data-type="codeBlock" text="{
    &quot;name&quot;: &quot;nft-web3-explorer&quot;,
    &quot;attributes&quot;: [
        {
            &quot;trait_type&quot;: &quot;tokenID&quot;,
            &quot;value&quot;: &quot;0&quot;
        }
    ],
    &quot;description&quot;: &quot;nft-web3-explorer image&quot;,
    //填入上面刚刚上传完成的图片地址
    &quot;image&quot;: &quot;ipfs://QmZ3Y31SwLU77CDfBoL5MphuSmrv414d2ZyunVcbNAJQRQ/0.png&quot;
}
"><code><span class="hljs-punctuation">{</span>
    <span class="hljs-attr">"name"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"nft-web3-explorer"</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"attributes"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"trait_type"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"tokenID"</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"value"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0"</span>
        <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"description"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"nft-web3-explorer image"</span><span class="hljs-punctuation">,</span>
    <span class="hljs-comment">//填入上面刚刚上传完成的图片地址</span>
    <span class="hljs-attr">"image"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"ipfs://QmZ3Y31SwLU77CDfBoL5MphuSmrv414d2ZyunVcbNAJQRQ/0.png"</span>
<span class="hljs-punctuation">}</span>
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a41718bc8236a3681c2179193ac6af4bf0ff7ca95a7fed25b8bfc1b7f913339e.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fd781ca0e341f620b11a66b607d5c04fd92b9dabb7bfa8774f8b75c7cf8ce109.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-baseuri" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">设置BaseURI</h4><p>我们需要将刚刚上传后的metadata文件地址设置给合约的baseURI，这样各个平台在使用tokenURI获取资源信息才可以获取到。</p><p>编写代码与合约交互设置BaseURI，在scripts目录下新建setBaseURI.js文件。</p><p>setBaseURI.js</p><pre data-type="codeBlock" text="require(&quot;dotenv&quot;).config()
const hre = require(&quot;hardhat&quot;);
const PRIVATE_KEY = process.env.PRIVATE_KEY
const NETWORK = process.env.NETWORK
const API_KEY = process.env.API_KEY


const provider = new hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
//编译完成合约会自动生成
const abi = require(&quot;../artifacts/contracts/NFT_WEB3_EXPLORER.sol/NFT_WEB3_EXPLORER.json&quot;).abi
const contractAddress = &quot;合约地址&quot;
const contract = new hre.ethers.Contract(contractAddress, abi, provider)
const wallet = new hre.ethers.Wallet(PRIVATE_KEY, provider)
const baseURI = &quot;metadata文件地址&quot;

async function main() {
  const contractWithSigner = contract.connect(wallet);
  //调用setBaseURI方法
  const tx = await contractWithSigner.setBaseURI(baseURI)
  console.log(tx.hash);
  await tx.wait();
  console.log(&quot;setBaseURL success&quot;);
}

main()
  .then(() =&gt; process.exit(0))
  .catch((error) =&gt; {
    console.error(error);
    process.exit(1);
  });
"><code><span class="hljs-built_in">require</span>(<span class="hljs-string">"dotenv"</span>).config()
const hre <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);
const PRIVATE_KEY <span class="hljs-operator">=</span> process.env.PRIVATE_KEY
const NETWORK <span class="hljs-operator">=</span> process.env.NETWORK
const API_KEY <span class="hljs-operator">=</span> process.env.API_KEY


const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
<span class="hljs-comment">//编译完成合约会自动生成</span>
const <span class="hljs-built_in">abi</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"../artifacts/contracts/NFT_WEB3_EXPLORER.sol/NFT_WEB3_EXPLORER.json"</span>).abi
const contractAddress <span class="hljs-operator">=</span> <span class="hljs-string">"合约地址"</span>
const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title"><span class="hljs-keyword">new</span></span> <span class="hljs-title">hre</span>.<span class="hljs-title">ethers</span>.<span class="hljs-title">Contract</span>(<span class="hljs-params">contractAddress, <span class="hljs-built_in">abi</span>, provider</span>)
<span class="hljs-title">const</span> <span class="hljs-title">wallet</span> = <span class="hljs-title"><span class="hljs-keyword">new</span></span> <span class="hljs-title">hre</span>.<span class="hljs-title">ethers</span>.<span class="hljs-title">Wallet</span>(<span class="hljs-params">PRIVATE_KEY, provider</span>)
<span class="hljs-title">const</span> <span class="hljs-title">baseURI</span> = "<span class="hljs-title">metadata</span>文件地址"

<span class="hljs-title">async</span> <span class="hljs-title"><span class="hljs-keyword">function</span></span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  const contractWithSigner <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.connect(wallet);
  <span class="hljs-comment">//调用setBaseURI方法</span>
  const <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await contractWithSigner.setBaseURI(baseURI)
  console.log(<span class="hljs-built_in">tx</span>.hash);
  await <span class="hljs-built_in">tx</span>.wait();
  console.log(<span class="hljs-string">"setBaseURL success"</span>);
}

main()
  .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
  .catch((<span class="hljs-function"><span class="hljs-keyword">error</span>) => </span>{
    console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
    process.exit(<span class="hljs-number">1</span>);
  });
</code></pre><p>在这里我们与合约交互主要是使用ethers，目前主流封装与合约交互的js库有2个web3.js与ethers.js，此处使用ethers的原因是hardhat默认和ethers配合。</p><pre data-type="codeBlock" text="//执行setBaseURI脚本
npx hardhat --network rinkeby run scripts/setBaseURI.js
"><code><span class="hljs-comment">//执行setBaseURI脚本</span>
npx hardhat <span class="hljs-operator">-</span><span class="hljs-operator">-</span>network rinkeby run scripts<span class="hljs-operator">/</span>setBaseURI.js
</code></pre><h3 id="h-mint" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">mint测试</h3><p>因为只有mint过的tokenID才会展示在交易市场中，所以我们需要编写代码进行mint测试(实际场景，该操作应该由前端页面调用完成)。mint.js</p><pre data-type="codeBlock" text="require(&quot;dotenv&quot;).config()
const hre = require(&quot;hardhat&quot;);
const PRIVATE_KEY = process.env.PRIVATE_KEY
const NETWORK = process.env.NETWORK
const API_KEY = process.env.API_KEY


const provider = new hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
const abi = require(&quot;../artifacts/contracts/NFT_WEB3_EXPOLRER.sol/NFT_WEB3_EXPOLRER.json&quot;).abi
const contractAddress = &quot;合约地址&quot;
const contract = new hre.ethers.Contract(contractAddress, abi, provider)
const wallet = new hre.ethers.Wallet(PRIVATE_KEY, provider)

async function main() {
    const contractWithSigner = contract.connect(wallet);
    //获取mint需要多少ETH
    const price = await contract.PRICE_PER_TOKEN();
    console.log(&quot;price is&quot; + price);
    //调用mint方法，支付mint费用
    const tx = await contractWithSigner.mint(1, { value: price});
    console.log(tx.hash);
    await tx.wait();
    console.log(&quot;mint success&quot;);
}

main()
    .then(() =&gt; process.exit(0))
    .catch((error) =&gt; {
        console.error(error);
        process.exit(1);
    });
"><code><span class="hljs-built_in">require</span>(<span class="hljs-string">"dotenv"</span>).config()
const hre <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);
const PRIVATE_KEY <span class="hljs-operator">=</span> process.env.PRIVATE_KEY
const NETWORK <span class="hljs-operator">=</span> process.env.NETWORK
const API_KEY <span class="hljs-operator">=</span> process.env.API_KEY


const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
const <span class="hljs-built_in">abi</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"../artifacts/contracts/NFT_WEB3_EXPOLRER.sol/NFT_WEB3_EXPOLRER.json"</span>).abi
const contractAddress <span class="hljs-operator">=</span> <span class="hljs-string">"合约地址"</span>
const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title"><span class="hljs-keyword">new</span></span> <span class="hljs-title">hre</span>.<span class="hljs-title">ethers</span>.<span class="hljs-title">Contract</span>(<span class="hljs-params">contractAddress, <span class="hljs-built_in">abi</span>, provider</span>)
<span class="hljs-title">const</span> <span class="hljs-title">wallet</span> = <span class="hljs-title"><span class="hljs-keyword">new</span></span> <span class="hljs-title">hre</span>.<span class="hljs-title">ethers</span>.<span class="hljs-title">Wallet</span>(<span class="hljs-params">PRIVATE_KEY, provider</span>)

<span class="hljs-title">async</span> <span class="hljs-title"><span class="hljs-keyword">function</span></span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
    const contractWithSigner <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.connect(wallet);
    <span class="hljs-comment">//获取mint需要多少ETH</span>
    const price <span class="hljs-operator">=</span> await <span class="hljs-keyword">contract</span>.PRICE_PER_TOKEN();
    console.log(<span class="hljs-string">"price is"</span> <span class="hljs-operator">+</span> price);
    <span class="hljs-comment">//调用mint方法，支付mint费用</span>
    const <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await contractWithSigner.mint(<span class="hljs-number">1</span>, { <span class="hljs-built_in">value</span>: price});
    console.log(<span class="hljs-built_in">tx</span>.hash);
    await <span class="hljs-built_in">tx</span>.wait();
    console.log(<span class="hljs-string">"mint success"</span>);
}

main()
    .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
    .catch((<span class="hljs-function"><span class="hljs-keyword">error</span>) => </span>{
        console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
        process.exit(<span class="hljs-number">1</span>);
    });
</code></pre><pre data-type="codeBlock" text="//执行mint脚本
npx hardhat --network rinkeby run scripts/mint.js
"><code><span class="hljs-comment">//执行mint脚本</span>
npx hardhat <span class="hljs-operator">-</span><span class="hljs-operator">-</span>network rinkeby run scripts<span class="hljs-operator">/</span>mint.js
</code></pre><h3 id="h-nft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">查看NFT</h3><p>此时我们去opensea或looksrare等NFT市场查看我们的NFT即可(个人建议looksrare查看，opensea测试网络速度很慢，而且metadata信息更新也比较慢)</p><ul><li><p>opensea测试网络地址:<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://testnets.opensea.io/">https://testnets.opensea.io/</a></p></li><li><p>looksrare测试网络地址:<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://rinkeby.looksrare.org/">https://rinkeby.looksrare.org/</a></p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5434a0a8a5a7eb7d4985084e6a58d731046e5bf56ad006b648d871e8bc14051e.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本篇文章我们介绍了如何一步步编写合约，部署以及与合约交互完成将NFT发布到链上，下篇文章将介绍如何搭建NFT的官网。</p><p>========</p><p>往期文章</p><p>Layer2: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483734&amp;idx=1&amp;sn=57f3834e10f6de540c4a1534ba8d9e70&amp;chksm=c1c9975cf6be1e4a2ac9d0d6608ee0b3f2994420ad805c02946915c3d90a7cdb15a10a018fae&amp;scene=21#wechat_redirect">真正理解 Layer2</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483774&amp;idx=1&amp;sn=aeffb5195a63c227410aa5f234b3d3bd&amp;chksm=c1c99774f6be1e62e03fad2647da7b3601459c7b5d3a254d8ed1ab4c9a4655d2de2e54a2b329&amp;scene=21#wechat_redirect">Immutable X白皮书(译)</a> 跨链: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483792&amp;idx=1&amp;sn=bee0ccb5301576342289bf48e806a9ad&amp;chksm=c1c9979af6be1e8cc188486e253e0bb9b15b1b3db7bcb02f6501c10a8bb8e3862290f45b51f2&amp;scene=21#wechat_redirect">关于Cosmos的研究</a> 基础理论: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483691&amp;idx=1&amp;sn=9e92b314e70d813f17d7766295eb7ab4&amp;chksm=c1c99721f6be1e379cb937216fd2443583c4f4a333c399ba0b2f5039165e15071850f98961e0&amp;scene=21#wechat_redirect">关于区块链不可能三角的研究</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483824&amp;idx=1&amp;sn=88599bec868ca1d2d85e8056fb7857fa&amp;chksm=c1c997baf6be1eac9128170e0d34504b4e57e21319fe02e405ba8243f7551e6afa56bb891fdb&amp;scene=21#wechat_redirect">关于零知识证明的研究</a> 以太坊: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;scene=21#wechat_redirect">以太坊技术系列-以太坊数据结构</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483704&amp;idx=1&amp;sn=8421aad1400b24aa38ec06a160220c03&amp;chksm=c1c99732f6be1e24fa8358e898fba6e73043688760b7feffd2c27d3008cfa71722f8ad1cad4b&amp;scene=21#wechat_redirect">以太坊技术系列-以太坊共识机制</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483681&amp;idx=1&amp;sn=4e6ccde6bee60503086b07734e08baf0&amp;chksm=c1c9972bf6be1e3dc094c893ca748a33f13180ebe7449d70b42dcb583d390bef2080bb800ddc&amp;scene=21#wechat_redirect">以太坊技术系列-钱包-以太坊中的账户</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483716&amp;idx=1&amp;sn=79b371ca4ad39ac02cdca46763e8ff26&amp;chksm=c1c9974ef6be1e5834b9cf367afd92f95c2f7b47920f36fdec17a7c7cce306e697e5a71e3d13&amp;scene=21#wechat_redirect">聊一聊智能合约</a> 去中心化存储: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483753&amp;idx=1&amp;sn=c6686a22cc8b34e55387c788fcaaf7cc&amp;chksm=c1c99763f6be1e759df17122d52c420c7af69e516212d1aa7bacc3d8cf76d52ba293a08abb31&amp;scene=21#wechat_redirect">去中心化存储的那些事(上)</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483761&amp;idx=1&amp;sn=7ac0f7ac5fb9c92631c954476e96c792&amp;chksm=c1c9977bf6be1e6d12de1c68c86c58c561ecb88fcb2790525ab05f534b3d5c6a7c5d656524b7&amp;scene=21#wechat_redirect">去中心化存储那些事(下)</a> 其他公链: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483807&amp;idx=1&amp;sn=116947704b21817f9f01b69f3adc4732&amp;chksm=c1c99795f6be1e834a4a502276613ef1200c70497e3e434566adbf94735e896011ede6f45643&amp;scene=21#wechat_redirect">Flow白皮书1-共识和计算分离(译)</a> 智能合约实战: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483847&amp;idx=1&amp;sn=ef48c48445cb1e621617b0455daede13&amp;chksm=c1c997cdf6be1edb78d77675d0e8fdae7df8a9454323ea8aecd26c0e9ead53b299cfc919928e&amp;scene=21#wechat_redirect">智能合约升级详解</a></p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/e8f4da7de8eb3565aa02d27550931450a52723de4e2da6a78bf048009bdc24f2.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[智能合约升级详解]]></title>
            <link>https://paragraph.com/@explorer-of-web3/IFBgSsTNrjS6S20PFR2V</link>
            <guid>IFBgSsTNrjS6S20PFR2V</guid>
            <pubDate>Wed, 20 Apr 2022 11:25:52 GMT</pubDate>
            <description><![CDATA[背景智能合约的特点之一就是部署到链上之后不能修改，这一机制使得合约的交互方都可以信任合约。但也带来了一系列的问题，其一如果已部署的合约发现漏洞，无法修复，其二如果已部署的合约要进行功能升级，很难对使用方无感知。所以我们希望能够想一种办法能够升级合约，解决上述问题。当然合约升级不仅仅是个技术问题，也是治理问题，因为合约如果可以被随意修改那么信任的基石将消失，所以需要有一系列的升级治理方案来对升级合约进行约束。本文主要讨论合约升级的技术实现，升级治理暂不讨论。目标梳理先看下我们希望达成的目标。6403.jpeg这是我们的直接目标，之前调用合约V1的调用者全部改为调用合约V2，但这样一来所有调用者都需要更改,只要有1个调用者漏改，那么他就会调用失败。有没有一种方案不需要调用者感知呢？计算机设计中"加一层"的思想又得到了充分应用，我们增加代理合约，调用方只对接代理合约，合约升级代理合约内部解决。6404.jpeg现在我们的目标就是思考如何实现核心的代理合约。升级原理升级合约中应该有2个方法:升级方法，需要每次设置实现合约(包含真正业务逻辑代码的合约)的地址，这个比较简单代理方法，这个方法...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">背景</h2><p>智能合约的特点之一就是部署到链上之后不能修改，这一机制使得合约的交互方都可以信任合约。但也带来了一系列的问题，其一如果已部署的合约发现漏洞，无法修复，其二如果已部署的合约要进行功能升级，很难对使用方无感知。所以我们希望能够想一种办法能够升级合约，解决上述问题。当然合约升级不仅仅是个技术问题，也是治理问题，因为合约如果可以被随意修改那么信任的基石将消失，所以需要有一系列的升级治理方案来对升级合约进行约束。本文主要讨论合约升级的技术实现，升级治理暂不讨论。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">目标梳理</h2><p>先看下我们希望达成的目标。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8177638500823b566e1238055e16fcbcae3b57f72f864eeeb074a4cb83d9e708.jpg" alt="6403.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6403.jpeg</figcaption></figure><p>这是我们的直接目标，之前调用合约V1的调用者全部改为调用合约V2，但这样一来所有调用者都需要更改,只要有1个调用者漏改，那么他就会调用失败。有没有一种方案不需要调用者感知呢？计算机设计中&quot;加一层&quot;的思想又得到了充分应用，我们增加代理合约，调用方只对接代理合约，合约升级代理合约内部解决。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/203562d17b8c5d2539e2847f00d17414266e11811211df0b536bcfedd1f133d6.jpg" alt="6404.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6404.jpeg</figcaption></figure><p>现在我们的目标就是思考如何实现核心的代理合约。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">升级原理</h2><p>升级合约中应该有2个方法:</p><ol><li><p>升级方法，需要每次设置实现合约(包含真正业务逻辑代码的合约)的地址，这个比较简单</p></li><li><p>代理方法，这个方法是连接调用者和实现合约的桥梁</p></li></ol><p>代理方法需要如何调用实现合约内的方法呢？我们需要先来研究一下合约之间方法调用有哪几种方式然后选择适合这个场景的来使用。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">调用合约的方式</h3><p><strong>直接调用</strong> 这种方式我们需要显示指定调用的方式，就像正常的函数调用一样，需要将合约地址传入构造出被调用合约。</p><pre data-type="codeBlock" text="/**
    调用者合约
**/
contract A {
    function x(address addr) public {
        B b = B(addr);
        b.y();
    }
}
/**
    被调用者合约
**/
contract B {
    uint public t;
    function y() public {
        t = t + 1;
    }

}
"><code><span class="hljs-comment">/**
    调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">x</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        B b <span class="hljs-operator">=</span> B(addr);
        b.y();
    }
}
<span class="hljs-comment">/**
    被调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">B</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> t;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">y</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        t <span class="hljs-operator">=</span> t <span class="hljs-operator">+</span> <span class="hljs-number">1</span>;
    }

}
</code></pre><p><strong>call调用</strong> 这种调用方式是使用ABI来对方法进行调用(对ABI不了解的小伙伴可以自行查阅，简单理解为将方法编码成二进制表示)</p><pre data-type="codeBlock" text="/**
    调用者合约
**/
contract A {
    function x(address addr) public {
        //获得方法对应的ABI，可以由外部传入
        bytes memory method = abi.encodeWithSignature(&quot;y()&quot;);
        (bool success, ) = addr.call(method);
    }
}
"><code><span class="hljs-comment">/**
    调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">x</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">//获得方法对应的ABI，可以由外部传入</span>
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> method <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSignature</span>(<span class="hljs-string">"y()"</span>);
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> addr.<span class="hljs-built_in">call</span>(method);
    }
}
</code></pre><p><strong>delegatecall调用</strong> 另外一种使用ABI方式来对方法进行调用，不过和call不同的是delegatecall调用不会切换上下文到目标合约中，下面会详细介绍。</p><pre data-type="codeBlock" text="/**
    调用者合约
**/
contract A {
    function x(address addr) public {
        //获得方法对应的bytes，可以由外部传入
        bytes memory method = abi.encodeWithSignature(&quot;y()&quot;);
        (bool success, ) = addr.delegatecall(method);
    }
}
"><code><span class="hljs-comment">/**
    调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">x</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">//获得方法对应的bytes，可以由外部传入</span>
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> method <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSignature</span>(<span class="hljs-string">"y()"</span>);
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> addr.<span class="hljs-built_in">delegatecall</span>(method);
    }
}
</code></pre><p>在这个例子中使用delegatecall，我们会发现在调用完x()方法后，B的t状态并没有变化。delegatecall是在本合约的上下文中去执行目标合约的方法逻辑，所以当方法执行寻找状态t时，也是在A合约去寻找而不是在B合约去寻找。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/18f9ce7543d41cf634d44f6a9b8fbc25874b3b7c83bdbaf4a575e49a085e0fa9.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>那么合约升级需要选择哪种调用方式呢？我们从2个方面角度考虑:</p><ol><li><p>代理合约到实现合约的调用要足够通用，因为代理合约不可更改,所以应该选用抽象程度较高的方式，不能将调用方法硬编码到合约中。</p></li><li><p>合约升级后，合约的状态不能丢失，因此合约状态不应该维护在可升级的实现合约中，而应该维护在代理合约中，状态在代理合约中我们的调用时也就不能进行上下切换到实现合约中去。</p></li></ol><p>从这2方面考虑，我们使用delegatecall来作为代理合约调用实现合约的方式。</p><p>我们来看一版最简实现代码</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
/**
    调用者合约
**/
contract Proxy {
    address public impl;
    event log(bytes);
    fallback () external payable{
        //msg.data即是被调用方法的ABI表述
        (bool success, ) = addr.delegatecall(msg.data);
        emit log(res);
    }
    //升级方法，如果需要升级调用该方法设置升级后的合约地址即可
    function setImpl(address addr) public{
        impl = addr;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-comment">/**
    调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Proxy</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> impl;
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">log</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span></span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">fallback</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span></span>{
        <span class="hljs-comment">//msg.data即是被调用方法的ABI表述</span>
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> addr.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
        <span class="hljs-keyword">emit</span> log(res);
    }
    <span class="hljs-comment">//升级方法，如果需要升级调用该方法设置升级后的合约地址即可</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setImpl</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span></span>{
        impl <span class="hljs-operator">=</span> addr;
    }
}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
/**
    实现合约 V1
**/
contract ImplV1 {
    uint public t;
    function addT() public payable {
        t = t + 1;
    }
    function getT() public returns (uint res){
        return t;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-comment">/**
    实现合约 V1
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV1</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> t;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        t <span class="hljs-operator">=</span> t <span class="hljs-operator">+</span> <span class="hljs-number">1</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> res</span>)</span>{
        <span class="hljs-keyword">return</span> t;
    }
}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
/**
    实现合约 V2
**/
contract ImplV2 {
    uint public t;
    function addT() public payable {
        t = t + 2;
    }
    function getT() public returns (uint res){
        return t;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-comment">/**
    实现合约 V2
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV2</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> t;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        t <span class="hljs-operator">=</span> t <span class="hljs-operator">+</span> <span class="hljs-number">2</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> res</span>)</span>{
        <span class="hljs-keyword">return</span> t;
    }
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8a5b0bbdb2a4dbd83c05d83630e8fcd7985a51c26af0c1d1661c2d8db743f4c4.jpg" alt="6405.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6405.jpeg</figcaption></figure><p>大家可以在remix上部署运行一下这3个合约，会发现1个问题，通过代理合约调用完addT()方法后，再调用getT()并未获得预期结果。这其实是因为代理合约和实现合约之间发生了存储冲突。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">存储冲突</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6fef3734662341e71f18fc3e9cd07de88da82e9d80ee8b69919cd7984b0fa648.jpg" alt="6406.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6406.jpeg</figcaption></figure><p>不仅会存在代理合约和实现合约的冲突，还会出现实现合约的不同版本之间的冲突。</p><p>如何解决存储冲突的问题，这里介绍一种个人比较喜欢的方案: 存储位置预留，如上文所述合约中的存储是按照声明顺序作为编号进行索引寻找的。发生冲突的原因是因为不同合约索引相同，如何能够让不同合约之前的状态的索引编号保证不同呢？使用继承，合约之间进行继承，其中的状态变量也就有了确定的顺序，不会存在重合的问题。</p><p>再来看下解决完存储冲突后的合约升级代码</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
//将存储单独拆1个合约(不需要部署)，方便进行继承
contract ProxyStore {
    address public impl;
    event log(bytes);
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-comment">//将存储单独拆1个合约(不需要部署)，方便进行继承</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ProxyStore</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> impl;
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">log</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span></span>)</span>;
}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ProxyStore.sol&quot;;
contract ImplV1Store is ProxyStore {
    uint public t;
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ProxyStore.sol"</span>;
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV1Store</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ProxyStore</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> t;
}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ImplV1Store.sol&quot;;
//因为本例中V2暂未新增状态
contract ImplV2Store is ImplV1Store {}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ImplV1Store.sol"</span>;
<span class="hljs-comment">//因为本例中V2暂未新增状态</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV2Store</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ImplV1Store</span> </span>{}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ProxyStore.sol&quot;;
/**
    调用者合约，继承自己的存储合约
**/
contract Proxy is ProxyStore{
    fallback () external payable{
        (bool success, bytes memory res) = impl.delegatecall(msg.data);
        emit log(res);
    }
    function setImpl(address addr) public{
        impl = addr;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ProxyStore.sol"</span>;
<span class="hljs-comment">/**
    调用者合约，继承自己的存储合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Proxy</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ProxyStore</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">fallback</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span></span>{
        (<span class="hljs-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> res) <span class="hljs-operator">=</span> impl.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
        <span class="hljs-keyword">emit</span> log(res);
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setImpl</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span></span>{
        impl <span class="hljs-operator">=</span> addr;
    }
}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ImplV1Store.sol&quot;;
/**
    实现合约 V1，继承自己的存储合约
**/
contract ImplV1 is ImplV1Store{
    // 
    function addT() public payable {
        t = t + 1;
    }

    function getT() public returns (uint res){
        return t;
    }

}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ImplV1Store.sol"</span>;
<span class="hljs-comment">/**
    实现合约 V1，继承自己的存储合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV1</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ImplV1Store</span></span>{
    <span class="hljs-comment">// </span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        t <span class="hljs-operator">=</span> t <span class="hljs-operator">+</span> <span class="hljs-number">1</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> res</span>)</span>{
        <span class="hljs-keyword">return</span> t;
    }

}
</code></pre><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ImplV2Store.sol&quot;;
/**
    实现合约 V2，继承自己的存储合约
**/
contract ImplV2 is ImplV2Store{
    function addT() public payable {
        t = t + 2;
    }
    function getT() public returns (uint res){
        return t;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ImplV2Store.sol"</span>;
<span class="hljs-comment">/**
    实现合约 V2，继承自己的存储合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ImplV2</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ImplV2Store</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        t <span class="hljs-operator">=</span> t <span class="hljs-operator">+</span> <span class="hljs-number">2</span>;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getT</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> res</span>)</span>{
        <span class="hljs-keyword">return</span> t;
    }
}
</code></pre><p>至此部分合约函数就拥有了升级能力，为什么是部分呢？fallback()函数是没有返回值的，当调用者调用1个原本有返回值的函数时，由于经过了代理合约的fallback()函数，所以无法将结果返回。我们有没有办法为fallback()也增加返回值呢？</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">返回值</h3><p>上层无法实现，但汇编操作可以为我们让我们实现这一能力。Solidity提供了Solidity Assembly让我们更友好地进行汇编操作，并且可以和Solidity语言混合使用。本文主题是合约升级，这部分我们不过多介绍。让我们来看看如何使用底层汇编为fallback()增加返回值。</p><p>看下实现代码，下面我们逐一介绍这里涉及到的指令</p><pre data-type="codeBlock" text="pragma solidity ^0.8.0;
import &quot;./ProxyStore.sol&quot;;
/**
    调用者合约
**/
contract Proxy is ProxyStore{
    fallback () external payable{
        (bool success, bytes memory res) = impl.delegatecall(msg.data);
        emit log(res);
        assembly {
            //分配空闲区域指针
            let ptr := mload(0x40)
            //将返回值从返回缓冲去copy到指针所指位置
            returndatacopy(ptr, 0, returndatasize())

            //根据是否调用成功决定是返回数据还是直接revert整个函数
            switch success
            case 0 { revert(ptr, returndatasize()) }
            default { return(ptr, returndatasize()) }
        }
    }
    function setImpl(address addr) public{
        impl = addr;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"./ProxyStore.sol"</span>;
<span class="hljs-comment">/**
    调用者合约
**/</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Proxy</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ProxyStore</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">fallback</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span></span>{
        (<span class="hljs-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> res) <span class="hljs-operator">=</span> impl.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
        <span class="hljs-keyword">emit</span> log(res);
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-comment">//分配空闲区域指针</span>
            <span class="hljs-keyword">let</span> ptr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)
            <span class="hljs-comment">//将返回值从返回缓冲去copy到指针所指位置</span>
            <span class="hljs-built_in">returndatacopy</span>(ptr, <span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>())

            <span class="hljs-comment">//根据是否调用成功决定是返回数据还是直接revert整个函数</span>
            <span class="hljs-keyword">switch</span> success
            <span class="hljs-keyword">case</span> <span class="hljs-number">0</span> { <span class="hljs-keyword">revert</span>(ptr, <span class="hljs-built_in">returndatasize</span>()) }
            <span class="hljs-keyword">default</span> { <span class="hljs-keyword">return</span>(ptr, <span class="hljs-built_in">returndatasize</span>()) }
        }
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setImpl</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span></span>{
        impl <span class="hljs-operator">=</span> addr;
    }
}
</code></pre><p>mload(0x40) //0x40地址会保存下一个可用地址 returndatacopy // 调用call或者delegatecall后其结果数据都会存储在返回值缓冲区中，returndatacopy方法可以将缓冲区的内容copy到指定地址 returndatasize // 返回值缓冲区数据的大小 switch //这里不使用if的原因是 Solidity Assembly的if不支持else return //返回 revert //回滚</p><p>其完整过程: 获得可用地址的指针，将返回缓冲区的数据copy到可用地址，根据调用是否成功，如果成功则返回可用地址内的数据，如果失败则进行回滚。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>我们通过delegatecall这一方式实现了合约升级，同时通过对合约状态存储的理解明白了为什么会存在存储冲突并采用继承的方式进行规避，最后我们的升级是建立在代理合约的fallback函数之上的，这个函数没有返回值，我们通过Solidity Assembly的方式进行数据返回。合约升级在技术上是不可或缺的手段，较好的使用可以帮助我们修复合约漏洞，对合约进行功能迭代，但如果使用不当则会造成较坏的影响，所以合约升级应该配合有效的升级治理方案。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/988f6e4124a5e51e7813040086d5184a02d9bfd5ac8733255baab2ed66cdfe9f.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[关于零知识证明的研究]]></title>
            <link>https://paragraph.com/@explorer-of-web3/tUnWQqUT9o5bcUcVjbwa</link>
            <guid>tUnWQqUT9o5bcUcVjbwa</guid>
            <pubDate>Fri, 15 Apr 2022 02:19:56 GMT</pubDate>
            <description><![CDATA[概述应用场景零知识证明在隐私保护、区块链扩容等领域都有非常重要的作用，在之前的文章中提到过零知识证明的有: 真正理解 Layer2 中我们介绍zk-rollup是Layer2一种较好的扩容方式,不需要较长的提款周期 Immutable X白皮书(译) 中介绍Immutable X这一Layer2扩容方案也是使用的零知识证明。 去中心化存储的那些事(上) 中的filecoin需要存储者证明自己确实进行了存储并且未被损坏(复制证明和时空证明)，这个证明过程也是使用零知识证明。 Flow白皮书1-共识和计算分离(译) flow采用了将共识和计算分离的方案，有专门的执行节点和验证节点，验证节点进行验证的时候也是使用知识证明减少计算量。 可见零知识证明在区块链中有着广泛应用，本文重点是介绍零知识证明的实现原理，所以这部分不过多赘述。简介6401.png我们首先需要把握零知识证明的完整主线，然后各种协议方案在这条主线上进行迭代更替就变换为不同的零知识证明的解决方案。引入问题零知识证明是为了达成证明者希望能够证明自己知道某一问题的答案但又不希望透露答案的目标。在这里我们首先引入问题。即如下图，...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">概述</h2><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">应用场景</h2><p>零知识证明在隐私保护、区块链扩容等领域都有非常重要的作用，在之前的文章中提到过零知识证明的有: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483734&amp;idx=1&amp;sn=57f3834e10f6de540c4a1534ba8d9e70&amp;chksm=c1c9975cf6be1e4a2ac9d0d6608ee0b3f2994420ad805c02946915c3d90a7cdb15a10a018fae&amp;token=1908335019&amp;lang=zh_CN#rd">真正理解 Layer2</a> 中我们介绍zk-rollup是Layer2一种较好的扩容方式,不需要较长的提款周期 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483774&amp;idx=1&amp;sn=aeffb5195a63c227410aa5f234b3d3bd&amp;chksm=c1c99774f6be1e62e03fad2647da7b3601459c7b5d3a254d8ed1ab4c9a4655d2de2e54a2b329&amp;token=1908335019&amp;lang=zh_CN#rd">Immutable X白皮书(译)</a> 中介绍Immutable X这一Layer2扩容方案也是使用的零知识证明。 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483753&amp;idx=1&amp;sn=c6686a22cc8b34e55387c788fcaaf7cc&amp;chksm=c1c99763f6be1e759df17122d52c420c7af69e516212d1aa7bacc3d8cf76d52ba293a08abb31&amp;token=1908335019&amp;lang=zh_CN#rd">去中心化存储的那些事(上)</a> 中的filecoin需要存储者证明自己确实进行了存储并且未被损坏(复制证明和时空证明)，这个证明过程也是使用零知识证明。 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483807&amp;idx=1&amp;sn=116947704b21817f9f01b69f3adc4732&amp;chksm=c1c99795f6be1e834a4a502276613ef1200c70497e3e434566adbf94735e896011ede6f45643&amp;token=1908335019&amp;lang=zh_CN#rd">Flow白皮书1-共识和计算分离(译)</a> flow采用了将共识和计算分离的方案，有专门的执行节点和验证节点，验证节点进行验证的时候也是使用知识证明减少计算量。<br>可见零知识证明在区块链中有着广泛应用，本文重点是介绍零知识证明的实现原理，所以这部分不过多赘述。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">简介</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8d088263a0446b4755cef80ac57c33ff3e1c869bb9028b3f13bfd3b85bc89fd1.png" alt="6401.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6401.png</figcaption></figure><p>我们首先需要把握零知识证明的完整主线，然后各种协议方案在这条主线上进行迭代更替就变换为不同的零知识证明的解决方案。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">引入问题</h2><p>零知识证明是为了达成证明者希望能够证明自己知道某一问题的答案但又不希望透露答案的目标。在这里我们首先引入问题。即如下图，零知识证明需要有1个问题才能够围绕这个问题进行证明。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4ec95f09992d05c4b8cfaf3919bee0f9dd9fbe210cdef3edf1dfd704d0361238.png" alt="6402.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6402.png</figcaption></figure><p>如果零知识证明需要对接无数种问题，那么方案是不具备通用性的，所以我们希望能够有一种问题可以将其他问题都转化为该问题，这样零知识证明就只针对该问题进行研究即可。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/51557ba94b0b607963cb252cd9c5e4fefae7ed153600641ebc0cff4ae8b21811.png" alt="6403.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6403.png</figcaption></figure><p>接下来我们就来寻找这类问题。</p><h2 id="h-npnpc" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">NP问题和NPC问题</h2><p>1个问题如果可以多项式时间内求解，我们可以称之为P问题。1个问题如果可以在多项式时间内通过解进行验证那么称之为NP问题。由此可见P问题是属于NP问题的，因为1个问题多项式时间内可以找到解，那么一定可以在多项式时间内验证。</p><p>这里简单介绍介绍下多项式时间求解是什么意思，学过数据结构和算法都了解时间复杂度O(n),如果1个算法时间复杂度是O(n) or O(n^2)n在底数位置那么称之为可以多项式时间内求解，如果1个算法时间复杂度O(n!) or O(2^n)等n在指数位置那么称之为不能在多项式时间内求解。</p><p>我们需要证明的问题一定是NP问题，否则无法在多项式时间内进行验证，而零知识证明非常重要的一个点计算验证要简单。那么有没有一种问题可以将所有的NP问题都转化为该问题呢？ NPC问题就是这样的一类问题，如果其他NP问题都可以约化为该问题，那么该问题称为NPC问题。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">问题描述-电路</h2><p>如何描述1个NPC问题，电路是1个经常被采用的方案。而且逻辑电路本身就是1个NPC问题。其他的描述方式我们将在后面进行介绍。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4c02f695f27366601a4054dac584eb78204ebab2e3bd785027228abf6fce114f.jpg" alt="640.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">640.jpeg</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/558a0eae81e40b200c2ead852c1180058757a160dce54f3abf5275ba20132188.png" alt="6404.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6404.png</figcaption></figure><p>注意:这里的画电路不是真正的用绘图方式表达，而是用电路语言进行编写。</p><h2 id="h-r1cs" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">数值化-r1cs</h2><p>接下来，我们的逻辑电路因为是要给计算机进行计算，所以我们希望将其数值化描述。这个数值化的过程通常会采用约束系统。比如常见的r1cs就是一种约束，其要求的约束是 A·s <em>B·s - C·s = 0这个约束如何理解呢？我们看个简单的例子: 25=k *</em> k 这个语句可以用来描述这样一个非常简单的电路</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/19386d780ecf229629cef0226715c966d5c929b79d0bfafc27e3eb3553eb0902.png" alt="6405.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6405.png</figcaption></figure><p>将电路中涉及到的变量组成1个向量s[k,25] 要有一组ABC使得 A·[k,25] <em>B·[k, 25] - C ·[k,25] = 0 等价于 25=k *</em> k 此时我们给出1组符合 A=[1,0] B=[1,0] C=[0,1] 代入 A·s <em>B·s - C·s = 0 后即可求得 25=k</em> * k</p><p>这里我们使用了最简单的例子，更复杂一点的例子可以参考V神较为知名的文章 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649">https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649</a></p><p>这里需要重点理解的就是电路是由很多个逻辑门操作组成的，而每个逻辑门操作总是可以转换成为2个输入1个输出。使用 A·s * B·s - C·s = 0 可以较好得通过数值描述每个逻辑门是针对输入做了什么样的变化从而转变成输出的。</p><p>当然电路除了r1cs外，还有一些其他的约束如:bacs、tbcs等，这里不再介绍。 我们编写的电路，经过r1cs的编译器，会编译成为符合r1cs规范的约束数值。这组数值就可以描述该电路，这样可以更方便地进行计算。 此时所理解的零知识证明变为:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/bdbe94cbd419a3766ab9c4cb0da6d71d446d64539115ef8d43e09d35d0052d32.png" alt="6406.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6406.png</figcaption></figure><p>在使用r1cs的时候问题转换为就是找到1个向量s满足A·s * B·s - C·s = 0，即使获得s，在实际问题中的验证过程我们可能需要验证数万个这样的逻辑运算，非常冗余，有没有办法进行一次运算即可验证呢？在这里我们引入多项式的方案。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">问题转化</h2><p>我们这里通过QAP问题介绍多项式的方案，QAP问题简单来说(这里经过简化)是需要找到p(x)能够整除一个已知的t(x),即可以表示为公式: p(x) = H(x) * t(x) 如何将r1cs转换为QAP呢，还是用我们上面的例子: 将r1cs中求得的向量A=[1,0] B=[1,0] C=[0,1]进行拉格朗日插值,插值方式为依次按照[1,A向量第一组第一个值] [2, A向量第二组第1个值]进行插值计算，求得的方程将系数提取作为A~ 向量组的第一个向量。依次类推A~ 向量的第二组方程是由A向量组的第2个值进行插值获得。关于拉格朗日插值大家可以自行搜索。求得: A~ [0,1] [0,0] B~ [0,1] [0,0] C~ [0,0] [0,1]</p><p>此时将s[k, 25]向量和A B C相乘可得: A~·s = [0,k] B~·s = [0,k] C~·s = [0,25]<br>p(x) = A~·s *B~·s - C~·s = 0 *x + k^2 -25 = k^2 -25<br>t(x) = x-1 因为无论p(x)的方程系数如何，总是满足x=1时 p(x) = 0 这个原因是因为我们在进行拉格朗日插值时使用了x=1作为插值点</p><p>证明者由于知道答案，自然也就知道p(x) = 0 验证者需要验证 符合条件的p(x) 满足 p(x)/t(x) = p(x) /(x-1) 能够整除。</p><p>在这里我们举了1个非常简单的例子，为了能够让大家在阅读时不再陷入复杂计算中去，较为复杂的例子还是可以阅读上面推荐的V神的那篇文章</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/84ea219d0d42c053cdebf144cf74442cc13ffa669cdee68652953dcc43e98ae6.png" alt="6407.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6407.png</figcaption></figure><p>很多算法都有类似QAP转换过程，目的如我们前文所说是为了简化验证。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">证明</h2><p>到了这里我们终于开始进行证明，常用的零知识证明会要求将问题转换为寻找1个满足特定条件多项式的问题(如上述的QAP)。直接将多项式系数给出是最简单的证明，但无法保证零知识。所以我们来讨论如何证明知道1个多项式，以上文中的QAP为例，需要寻找到P(x)能够整除t(x),进行公式化表达即: P(x) = H(x) * t(x)</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/09fbe9c7dcbdbdbf2b942a519ea1d0880f22ac6d93f87f690709de2b7d581cd2.png" alt="6408.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6408.png</figcaption></figure><p>如上图所示 验证者发起挑战，向证明者发送随机数r，如果证明者可以给出 P(r)和H(r)，验证者进行验证t(r) = P(r)/H(r)，这样进行多次随机挑战降低证明者欺骗的概率从而使得验证者相信证明者的确知道p(x)。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ca1f9516c8ee32c253ea13755cb7c99aad05e851cec21cc6aa4b41d6c7862b49.jpg" alt="6401.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6401.jpeg</figcaption></figure><p>但这里存在1个问题，因为t(x)和r都是公开的，所以此时验证者可以随意构造p(r)和H(r)使得t(r) = p(r) / H(r)。我们需要有一种方式对r进行加密，但因为r要参与计算，所以加密后的r计算结果要能够通过r加密计算求得，这样才可以进行验证，这里就用到了同态加密。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">同态加密</h2><p>同态加密即加密后的运算结果可以和先运算再加密进行转换，比如: g^a * g^b = g^(a+b) 我们暂且使用S(x) = g^x 作为同态加密函数，来看看上面说的r泄露问题如何解决 举个例子: P(x) = 2x^2 -x<br>t(x) = x<br>H(x) = 2x-1</p><p>验证者依旧随机挑选1个数r，但这次不公开r，而是公开g^r。我们使用的是加法同态: S(x)S(y) = g^(x+y) 也就是说g^(x^2)无法通过g^x运算得到,所以验证者需要将g^(r^2)和g^r都给到证明者。</p><p>证明者获取到g^(r^2)和g^r后利用同态性质计算:<br>g^(p(r)) = g^(2r^2-r) = (g^(r^2))^2 /g^r g^(H(r)) = g^(2r-1) = (g^r)^2 /g<br>证明者计算出g^(p(r))、g^(H(r))后发送给验证者</p><p>验证者拿到g^(p(r))和g^(H(r))后进行验证<br>g^p(r)= g^(H(r)* t(r)) = (g^H(r))^t(r)</p><p>至此，我们通过同态加密解决了，随机数r泄露的问题。</p><p>上面的同态加密解决了证明者利用r泄露欺骗验证者的问题，那么验证者是否会通过多轮随机数挑战将多项式系数获取到呢，如果存在这种可能的话就无法保证零知识了，我们先来看看会不会发生这种情况。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">零知识</h2><p>我们假设验证者进行3次挑战给出的随机数是r1，r2, r3那么验证者将会知道 (r1,P(r1)) (r2,P(r2)) (r3,P(r3)),对于2次方程通过3个点可以求得原方程，所以说验证者是可以通过多轮随机挑战来窃取答案的。 那么如何避免这种情况呢？我们这里让证明者生成随机数l，在每次结果中增加随机偏移值。整个过程变为证明者生成随机数L，验证者生成随机数r，证明者计算出结果P(r) H(r)使用l进行变换。注意，这里l依旧需要进行同态加密发送给验证者或者进行计算的时候消除掉这个l。</p><p>按照上面例子 我们进行变换，证明者返回 (g^p(r))^l和(g^H(r))^l<br>那么验证者验证 (g^p(r))^l= (g^(H(r)* t(r))^l = ((g^H(r))^l)^t(r) 即可</p><p>现在我们保障了证明者不会欺骗验证者，验证者也不会从证明者获取到原问题有关的知识。但刚刚有没有发现，我们的示例都是基于验证者使用随机数去挑战证明者，这样他们2者之间是有交互的。如果网络中存在大量的验证者，那么证明者需要和每个验证者进行通信，通信成本很大。有没有办法，使得证明者直接给出证明，所有的验证者拿着这个证明就可以对结果进行验证了呢？</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">非交互式</h2><p>使用非交互式，这里意味着没有验证者会持有随机数r，因为证明者的这份证明是针对所有验证者的，除非所有验证者都持有r，那这样证明者也大概率会知道r。所以我们不能直接公开r，需要使用避免泄露的方式同态加密R=g^r来进行公开，但需要注意的是上面介绍同态加密的时候我们进行验证的方式是:<br>g^p(r)= (g^H(r))^t(r)<br>但此时验证者并不知道t(r),我们只知道加密后的结果g^t(r)。我们需要一个函数能够满足乘法同态，即:<br>S(a)S(b) = S(a* b)<br>这样我们验证就可以通过: S(p(r)) = S(H(r) *t(r)) = S(H(r)) * S(t(r)) 来进行验证。</p><p>椭圆曲线pairing具备这样乘法同态的性质，具体可以自行查阅资料。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f5597eeeda8d37cff695e139451297b5c19db47c37e51d5e2b51dafff95711f6.jpg" alt="6402.jpeg" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6402.jpeg</figcaption></figure><h3 id="h-crs" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">CRS</h3><p>在上文中，我们介绍非交互式实现的方式是通过将随机数r加密生成R，这一过程需要有可信的第三方完成，并且生成完成需要立马删除原始r。这一过程使用的就是CRS，CRS是指通过一种受信任的方式生成一组可以被公开访问的字符串(一系列值)。这也就是SNARK中经常提到的初始化可信设置，由上面的推导，我们可以思考得出如果原始值r泄露，那么证明者可以随意欺骗验证者。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">其它方案</h2><p>上述我们大部分都是使用zk-SNARK的过程，但这只是1个流程，其中每个阶段都会有其他方案，比如我们会提出以下一些问题</p><ol><li><p>上文我们介绍证明时是通过验证者向证明者发起挑战的方式，为了避免随机挑战数被泄露使用了同态加密，那么有没有其他方式证明呢？</p></li><li><p>NP问题描述方式只有电路这一种？</p></li><li><p>为什么STARK是抗量子攻击的特性呢？</p></li></ol><p>我们简要回答下这几个问题</p><ol><li><p>Q: 上文我们介绍证明时是通过验证者向证明者发起挑战的方式，为了避免随机挑战数被泄露使用了同态加密，那么有没有其他方式证明呢？ A: 除了隐藏询问参数，另外一种方式是多项式承诺，plonk就使用了该方案，很多STARK应进行采用。多项式承诺可以做到不需要设置可信参数，这也是STARK的优势之一。</p></li><li><p>Q:NP问题的描述方式只有电路吗？ A: 描述NP问题的方式一般有这几种 1.电路 2.RAM 3.高级编程语言 RAM描述方式和汇编语言可以认为是类似的表达形式。这几种方式在表达上是越来越好，但底层处理上是越来复杂了。RAM也需要进行数值化，RAM经过数值化后称之为AIR。 一般而言STARK不再使用电路来描述问题(自然也就不会使用r1cs约束)，而是使用RAM(数值化会变为AIR)。</p></li><li><p>Q:为什么STARK是抗量子攻击的特性呢？ A: 因为SNARK大多数采用非对称加密而STARK大部分采用对称加密，对称加密是具备抗量子性的，非对称加密是不具备抗量子性的。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本文对零知识证明的过程进行分析，希望读者可以对零知识证明的整体流程有所了解，后续讨论零知识证明原理的时候我们都是在放大讨论这篇文章中的某一部分，再去接触一些令人眼花缭乱的技术时，我们能够很清晰的知道，这项技术是解决了零知识证明的哪部分问题，针对哪部分进行了优化，就像文末介绍的较新的STARK中的AIR其实是用来替换r1cs作为另外一种描述NP问题的方式。</p><p>参考 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eprint.iacr.org/2012/215.pdf">https://eprint.iacr.org/2012/215.pdf</a> (介绍QAP问题的论文） <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/scipr-lab/libsnark">https://github.com/scipr-lab/libsnark</a> (实现SNARK的库) <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/iden3/snarkjs">https://github.com/iden3/snarkjs</a> (使用js实现SNARK，支持groth16和plonk) <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://nakamoto.com/cambrian-explosion-of-crypto-proofs/">https://nakamoto.com/cambrian-explosion-of-crypto-proofs/</a> (各种证明之间不同维度的对比) <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649">https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649</a> (V神针对ZK-SNARK的介绍)</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/d4abd1499db2246d9e13ca10d684b775de5ffd9f0d54a4608c3529683a86804b.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Immutable X白皮书(译) ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/immutable-x</link>
            <guid>VePa22VNRFsikPZxrMg3</guid>
            <pubDate>Mon, 04 Apr 2022 02:33:29 GMT</pubDate>
            <description><![CDATA[本文翻译自Immutable X白皮书，不带有个人观点。也不构成任何投资建议。概述数字资产所有权的未来基于以太坊NFT的限制3.1 吞吐量低3.2 用户体验差3.3 开发速度慢3.4 流动性差Immutable X:以太坊NFT的下一代协议4.1 ZK-Rollup扩展引擎4.2 API抽象层4.3 支持NFT的钱包4.4 平台SDK4.5 共享流动性和订单簿4.6 为世界级合作伙伴提供合规支持4.7 官方市场和交易历史浏览协议架构和功能5.1 Rollup设计5.1.1 Vault Merkle Tree5.1.2 STARKs over SNARKS5.1.3 数据可用性5.2 资产铸造5.3 交易签名5.4 费用1.概述Immutable X是以太坊NFT的L2扩展解决方案。具有即时交易，大规模处理，铸造和交易NFT零gas费用的特点，而且不会影响用户或资产的安全性。Immutable X使用StarkWare强大的STARK验证器和rollup技术开发，是迄今为止最先进的NFT扩展技术解决方案。2.数字资产所有权的未来NFTs是数字资产所有权的未来。NFT交易总量已经从2...]]></description>
            <content:encoded><![CDATA[<p>本文翻译自Immutable X白皮书，不带有个人观点。也不构成任何投资建议。</p><ol><li><p>概述</p></li><li><p>数字资产所有权的未来</p></li><li><p>基于以太坊NFT的限制</p><ul><li><p>3.1 吞吐量低</p></li><li><p>3.2 用户体验差</p></li><li><p>3.3 开发速度慢</p></li><li><p>3.4 流动性差</p></li></ul></li><li><p>Immutable X:以太坊NFT的下一代协议</p><ul><li><p>4.1 ZK-Rollup扩展引擎</p></li><li><p>4.2 API抽象层</p></li><li><p>4.3 支持NFT的钱包</p></li><li><p>4.4 平台SDK</p></li><li><p>4.5 共享流动性和订单簿</p></li><li><p>4.6 为世界级合作伙伴提供合规支持</p></li><li><p>4.7 官方市场和交易历史浏览</p></li></ul></li><li><p>协议架构和功能</p><ul><li><p>5.1 Rollup设计</p><ul><li><p>5.1.1 Vault Merkle Tree</p></li><li><p>5.1.2 STARKs over SNARKS</p></li><li><p>5.1.3 数据可用性</p></li></ul></li><li><p>5.2 资产铸造</p></li><li><p>5.3 交易签名</p></li><li><p>5.4 费用</p></li></ul></li></ol><h2 id="h-1" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1.概述</h2><p>Immutable X是以太坊NFT的L2扩展解决方案。具有即时交易，大规模处理，铸造和交易NFT零gas费用的特点，而且不会影响用户或资产的安全性。Immutable X使用StarkWare强大的STARK验证器和rollup技术开发，是迄今为止最先进的NFT扩展技术解决方案。</p><h2 id="h-2" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2.数字资产所有权的未来</h2><p>NFTs是数字资产所有权的未来。NFT交易总量已经从2020年上半年的13.7亿美元上涨到2021上半年的25亿美元，并且我们仅仅发掘了NFTs表面的一部分。现在，我们看到主流艺术家启动NFT项目，大品牌进入这个领域，现有的Difi项目(eg: Uniswap)将NFTs作为他们产品的核心部分，我们才刚刚开始。 与此同时，以太坊也经历了历史上所有网络中最陡峭的成长曲线。以太坊市场价格从一年前不到500亿美元上升到现在超过3000亿美元。数百万用户现在使用以太坊去中心化应用(DApps)。并且每天数十亿美元的交易在网络上被处理。以太坊在NFT区块链中占据优势，拥有很多不同体量的应用。重要的是，几乎所有区块链和NFT的创新都发生在以太坊上，其他网络只是在复制以太坊的应用生态系统。这个生态系统增长很快-仅在2021年就有数百个NFT项目被启动,他们都在尝试抓住历史上最大的数字化变革。</p><p>以游戏这类NFT项目的市场为例，游戏是目前 NFT 开发者的主要方向之一，其理由充分：玩家新技术的早期接受者，游戏是拥有很大的市场。游戏目前是一个价值 3000 亿的产业，并且有望在 2021 年至 2025 年期间每年增长约10%，它的规模超过了体育，电影和音乐产业的总和。自 2018年以来，玩家已经在游戏内资产上花费超过1000亿美元，例如独特的角色皮肤，道具和解锁特定内容。这些内容是NFT的绝佳机会：这些资产如果使用NFT所代表对于用户来说会更有价值。</p><p>所有独特且可交易的内容，包括艺术品，游戏内资产和实体商品都将被公链上的 NFTs 所代表。视频，音乐或图片等数字内容可以变成具有稀缺性的数字藏品，从而形成一种新型的创作者-受众关系。独特的实物商品，如钻石、稀有金属，以及房地产或运动鞋等财产也可以被代币化为NFTs。作为NFTs，这些资产将在全球流动性市场上进行交易，并能够使用为NFTs交易构建的所有工具(如租赁、衍生品、交易所、钱包)。</p><p>多年来，数字商品相对实体商品有一些不足:没有稀缺性，没有真正的所有权，并且受到诸多限制。随着我们的生活越来越数字化，未来NFTs的所有权是明确的:数字商品不仅等同于实体商品，还会更好。</p><p>那么为什么不是每一独特的数字资产都是以太坊NFT呢？</p><h2 id="h-3nft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.基于以太坊NFT的限制</h2><p>尽管以太坊整体上占据主导地位，而且对NFT有明显的需求，但以太坊要成为将NFT扩展到全球受众的底层平台，还需要克服大量障碍。越来越多的区块链正在竞争NFT交易市场，通常通过牺牲以太坊的安全性来提高其吞吐量和改善其用户体验。以太坊面临的主要挑战是:</p><h2 id="h-31" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.1 吞吐量低</h2><p>与传统数据库相比，以太坊为了维护其安全性和去中心化，保持了较低的吞吐量:每秒5到15个事务(TPS)。因此，以太坊上活动的大规模增长导致了网络拥塞和缓慢的交易时间。它还提高了gas费用，导致交易对许多用户来说变得不划算，导致开发商和交易商被定价出局。即使在以太坊2.0迁移完成之后，交易吞吐量也会受到限制，交易成本也会让主流游戏中的小交易望而却步。</p><h2 id="h-32" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.2 用户体验差</h2><p>目前，以太坊上 NFT 交易的用户体验还没有为大众采用做好准备。交易需要几分钟或几小时才能确认，而且往往无法成功，从而极大地影响用户满意度、价格发现和整体流动性。现有的解决方案或替代区块链只能通过损害用户资产安全或网络去中心化来改善这种用户体验。此外，NFT钱包体验通常较差——它们令人困惑，恢复机制很差，并且没有显示必要的信息来帮助用户做出明智的购买决定。</p><h2 id="h-33" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.3 开发速度慢</h2><p>目前，NFT应用程序或市场开发人员将大部分开发时间花在构建和配置项目的区块链组件上。这让他们忽视了真正重要的东西:围绕真正的资产所有权为用户创造奇妙而独特的体验。开发人员需要学习新的编程语言和范例，然后立即使用它们编写对安全性至关重要的应用程序。他们没有用于创建应用程序的工具——没有简单的api，没有特定平台的sdk，没有预构建的基础设施。这为新项目的进入创造了巨大的障碍，并阻止了许多NFT的启动。</p><h2 id="h-34" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.4 流动性差</h2><p>由于其异质性，NFT比可替代的 ERC-20对应物的流动性要低。每个 NFT都必须根据其唯一ID进行买卖。这意味着，要花费1ETH购买每一条“gold dragon”NFT，您可能需要打开数百个买入订单——这些订单甚至不会涵盖新铸造的“gold dragon”。这个问题存在一些解决方案，但它们显着增加了铸币和交易的成本。个别市场在改进这一问题过程中投入了大量资金，但这些方案通常会进一步分散流动性，对价格发现产生负面影响。最终，这不仅会影响个人用户，还会影响整个 NFT 市场，因为交易者无法快速出售资产，也无法获得尽可能好的价格。</p><h2 id="h-4-immutable-xnft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4 Immutable X:以太坊NFT的下一代协议</h2><p>Immutable 认为 NFT 用户和开发人员不应该在以太坊的安全性和吞吐量做出选择，并为他们的用户创造世界级的体验。 Immutable X 是 NFT 的尖端协议，它使项目能够在以太坊的Layer2上构建，并具有出色的开发体验和用户体验。这是通过几个核心组件实现的：</p><h2 id="h-41-zk-rollup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.1 ZK-Rollup扩展引擎</h2><p>用以太坊联合创始人 Vitalik Buterin 的话来说，“中短期以太坊生态系统很可能全力以赴rollips以提升吞吐量”。 Immutable X将NFT特定的交换和证明逻辑与StarkWare开发的尖端Rollup技术结合，用于创建有史以来最先进的 NFT 交易工具。 Rollups 允许世界级的可扩展性和用户体验，同时保留以太坊的底层安全性。他们通过批量处理大量交易，为这些交易生成“有效性证明”，然后将该证明提交给 L1 智能合约来做到这一点。 Immutable X 的 rollup 允许每秒超过 9,000 次 NFT 转移、交易和铸币，达到并超过主流 NFT 项目所需的规模。有关 Immutable 的汇总实现的详细信息，请参见第 5 节。</p><h2 id="h-42-api" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.2 API抽象层</h2><p>为了更轻松地构建 NFT 应用程序，Immutable X 将这个扩展引擎封装在一组强大的 REST API 中。在 Immutable X 上，每一次交互，从铸造到交易再到转账，都像 API 调用一样简单。将可能需要几分钟或几小时的复杂异步区块链交互转换为同步RESTAPI 调用，是对现有区块链开发范式的强大升级。无需直接与智能合约交互，该领域的新进入者，例如成熟的游戏和内容公司，可以更快地推出更好的项目。</p><h2 id="h-43-nft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.3 支持NFT的钱包</h2><p>与其他区块链或侧链不同，Immutable X 目前支持所有桌面以太坊钱包，无需强制用户切换网络。该协议提供了一个中间层(&quot;Link&quot;)可实现特定于NFT钱包的世界级体验。这也使Immutable X 能够支持蓬勃发展的第三方市场生态系统，而不会带来安全风险：用户可以依靠 Immutable X Link 来确保他们不会被他们购买的资产所欺骗。</p><h2 id="h-44-sdk" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.4 平台SDK</h2><p>平台和特定编程语言的SDK允许合作伙伴更轻松地与 Immutable X 集成。目前，有一个 Typescript SDK的实现可以使用，可用于将协议轻松集成到网站中。 SDK允许对Immutable X API 和 Wallet进行访问。未来，将为所有常见的编程语言以及 Android、iOS、Unity 和 Unreal 等开发平台提供SDK。结合 API，Immutable X SDK 将允许合作伙伴在数小时而不是数周内构建NFT项目。</p><h2 id="h-45" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.5 共享流动性和订单簿</h2><p>与其他 NFT 扩展解决方案不同，Immutable X 提供共享的全球订单簿以促进协议流动性。这意味着在一个市场上创建的订单可以在另一个市场上完成，从而促进更有效的市场引导和价格发现。这也意味着 NFT 市场可以建立在 Immutable X 上而无需后端服务。 Immutable 认为，提升NFT流动性的一个关键方法是允许这些NFT在不同市场(针对不同用户)之间交易——我们希望看到在 Immutable X 上发展出蓬勃发展的第三方市场生态系统！</p><h2 id="h-46" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.6 为世界级合作伙伴提供合规支持</h2><p>目前，世界上一些大公司正在探索 NFT。然而，他们经常担心 NFT 的监管影响，包括版权保护和 AML/KYC 立法。 Immutable X 将提供一个完全合法的平台，并且能够成为这些大型项目的可信赖合作伙伴，以便将它们保留在以太坊上。我们认为在不牺牲用户安全情况下，购买NFT应该像在传统电子商务中购物一样简单。</p><h2 id="h-47" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4.7 官方市场和交易历史浏览</h2><p>Immutable X Marketplace 的存在是为了在协议上交易NFT提供官方主页，让用户和开发人员相信总会有交易的地方。 Immutable X Marketplace 还降低了没有资源创建自己交易市场的内容创建者和小型开发者的进入门槛。验证用户的交易历史对于促进区块链生态系统的信任和合法性很重要，Immutable X 也不例外，任何人都可以验证交易和查看历史状态。</p><h2 id="h-5" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5 协议架构和功能</h2><h2 id="h-51-rollup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5.1 Rollup设计</h2><p>Immutable X 的核心是 zk-rollup 扩展引擎，它是与StarkWare合作开发的，使用了他们的StarkEx证明生成器和验证器。首先，让我们概括地讨论一下 rollup：在 zk-rollup 中，用户将资产锁定在链上智能合约中：资产被“存入”L2。这些资产可以在L2进行交易，交易由“操作员”排序成一致的历史序列。然后将交易分组为“批次”或“块”，并为该批次的有效性生成证明。然后，该证明会在链上发布，并由更新链上状态的智能合约进行验证。通过链上状态变更，用户可以解锁他们的资产（从rollup中“提取”）。</p><p>为了更好地支持下一代 NFT 的需求，我们做出了几个重要的设计决策：</p><h3 id="h-511-vault-merkle-tree" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.1.1 Vault Merkle Tree</h3><p>Immutable X 的“资产状态”表示为一棵巨大的 Merkle 树，其中每个叶节点都是一个包含资产的Vault，而其他每个节点都是其两个子节点的哈希值。这棵树的编码在2个地方被使用：</p><ul><li><p>首先，在管理存款、取款和状态更新的链上智能合约验证器中。该合约存储了默克尔树的根，并确保只有在存在有效证明的情况下才能更新该根，因此Vault Merkle Tree不可能转换为无效状态。目前，该合约可以通过时间锁进行升级——目的是最终确保合约逻辑不可变。</p></li><li><p>其次，在用 StarkWare 的 Cairo 语言编写的 L2 证明逻辑中，它确定了有效状态转换的要求（例如，验证用户转移的资产是否归自己所有）。</p></li></ul><h3 id="h-512-starks-over-snarks" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.1.2 STARKs over SNARKS</h3><p>Immutable X 使用 STARK 证明，而不是更常见的 SNARK 证明。 STARK 证明是证明技术的最新进展，旨在解决 SNARK-rollups 的关键问题，即：</p><ul><li><p>SNARK 需要可信设置</p></li><li><p>SNARK 不是量子安全的</p></li><li><p>SNARK 依赖于极其复杂的密码学，并且容易出现实施错误</p></li></ul><p>STARK 证明更大，在链上发布的成本更高：我们认为这是一个可以接受的权衡，以提高用户安全性。</p><h3 id="h-513" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.1.3 数据可用性</h3><p>如果 Immutable 变得完全没有响应并且协议开发停止，用户将需要他们的交易数据可用以提取他们的资产。 Immutable X 支持两种数据可用性模式：rollup 和 validium。在rollup 模式下，每个批次之间的状态变化都会发布到 L1，保证L1的安全性，但会为每个事务增加一个小的线性成本。在validium模式下，数据可用性委员会 (DAC) 会对每个批次进行签名，以表明他们保留了数据的副本。即使只有一名委员会成员诚实，用户也将能够成功退出协议。目前，Immutable 的 DAC 包括：Immutable、StarkWare、Deversifi、Consensys、Nethermind、Iqlusion、Infura 和 Cephalopod。在这两种情况下，系统都是去中心化的：即使 Immutable 消失、被黑客入侵，协议也将能够恢复并且用户资产是安全的。</p><h2 id="h-52" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5.2 资产铸造</h2><p>Immutable X 的主要优势之一是NFT可以完全在 L2 中铸造，同时保留 L1 的安全性。每个NFT都可以用不可变的元数据铸造，在NFT提取时将传递给 L1 智能合约。该技术已被用于在 Immutable X 上铸造超过 1000 万个 NFT（超过 L1 上的所有 ERC721 NFT）。这些NFT还可以具有“可变元数据”，它将定期从铸币应用程序的服务器轮询，并且每个应用程序都可以选择提供一个“元数据标识符”，它将通知应用程序每个NFT的可变元数据属性的理想表示。不可变元数据和可变元数据的这种组合支持NFT用于表示每种类型的数字资产 - 从体验等级不断变化的游戏资产到基于确定的初始种子叠加不同用户输入的生成艺术品NFT。</p><h2 id="h-53" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5.3 交易签名</h2><p>在ImmutableX上进行交易，用户需要STARK-friendly椭圆曲线生成的专属密钥对。为了解决这个问题，我们构建了“Link”，作为以太坊钱包和 Immutable X 之间的中间层。用户签署安全消息，该消息用作生成STARK密钥对的种子。这将“委托”ImmutableX 钱包的安全和恢复给用户的以太坊钱包，即使STARK密钥对丢失，用户也可以使用新的以太坊签名重新生成它。在Link内部，这个 STARK 密钥对随后用于交易签名，每个签名的精确编码由交易类型决定。</p><h2 id="h-54" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5.4 费用</h2><p>费用是Immutable X 协议的主要收入来源。目前，Immutable X 对每笔 NFT 交易收取 2% 的费用（以购买货币计价）。由于我们对 Immutable X 费用的目标是协调应用程序开发人员、市场和交易者之间的激励措施，因此其他生态系统参与者也将能够设定费用：</p><p>1.市场还可以为每个订单添加自己的费用。由于 Immutable X 提供了一个可供所有市场访问的共享订单簿，因此在一个市场上创建的订单可以在另一个市场上完成，两者都收取费用。 2.NFT的创造者可以对他们铸造的每个 NFT设置使用费，以便从后续每次购买该资产中收取费用。使用费是NFT出售价格的固定百分比，在NFT铸造后无法调整。</p><p>NFT创造者和市场之间的这种激励机制为游戏等企业或艺术家等个人创作者开辟了全新的收入来源。他们之前从未能够从作品的持续升值而受益。这个系统给了创造者巨大的动力，使得创造者让这些NFT对目前持有的人具备价值。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/ea7070237b48a7446825e14d76beca7be41b61274bc44784dcb2dfb6cd92a258.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[以太坊技术系列-以太坊共识机制]]></title>
            <link>https://paragraph.com/@explorer-of-web3/iLEuDuhOxksMTWaSXmS8</link>
            <guid>iLEuDuhOxksMTWaSXmS8</guid>
            <pubDate>Sun, 03 Apr 2022 07:04:33 GMT</pubDate>
            <description><![CDATA[本篇文章主要讨论了共识机制是解决分布式系统中的拜占庭将军问题，以及分析了以太坊中的共识机制一般包括最长链选择和一种sybil抗性机制(pow或pos)。重点分析了pow和pos的流程以及设计思想。后续将开始重点讨论智能合约的部分。什么是共识机制分布式系统引入的恶意节点区块链的特点之一是去中心化。也就是节点会分布在各个地方组成分布式系统。各个节点需要对1个问题达成一致，理想情况下，只需要同步状态即可。6402.png如上图所示 B节点将a=1=> a=2的状态同步给 ACDE四个节点，这时系统中状态变为a=2, 但如果其中有恶意节点 AE 收到通知后把a=1=>a=3修改为错误的节点，这个时候大家的状态就不一致了，此时需要共识机制使系统中得到1个唯一正确的状态。拜占庭将军问题如上面说到分布式系统存在恶意节点导致系统中状态不一致的情况有1个比较著名的虚拟问题-拜占庭将军问题。 拜占庭将军问题是指，N个将军去攻打一座城堡，如果大于一定数量的将军同时进攻则可以攻打成功，如果小于则进攻失败。将军中可能存在叛徒。6403.png这个时候有2种情况 1.如果2个叛徒都在BCDE中，那么共识算法...]]></description>
            <content:encoded><![CDATA[<p>本篇文章主要讨论了共识机制是解决分布式系统中的拜占庭将军问题，以及分析了以太坊中的共识机制一般包括最长链选择和一种sybil抗性机制(pow或pos)。重点分析了pow和pos的流程以及设计思想。后续将开始重点讨论智能合约的部分。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">什么是共识机制</h2><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">分布式系统引入的恶意节点</h2><p>区块链的特点之一是去中心化。也就是节点会分布在各个地方组成分布式系统。各个节点需要对1个问题达成一致，理想情况下，只需要同步状态即可。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/878520e80ae54455ba5c8dab66daa55e851f749b8c7b136a19bc7ad91fe90543.png" alt="6402.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6402.png</figcaption></figure><p>如上图所示 B节点将a=1=&gt; a=2的状态同步给 ACDE四个节点，这时系统中状态变为a=2, 但如果其中有恶意节点 AE 收到通知后把a=1=&gt;a=3修改为错误的节点，这个时候大家的状态就不一致了，此时需要共识机制使系统中得到1个唯一正确的状态。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">拜占庭将军问题</h2><p>如上面说到分布式系统存在恶意节点导致系统中状态不一致的情况有1个比较著名的虚拟问题-拜占庭将军问题。</p><p>拜占庭将军问题是指，N个将军去攻打一座城堡，如果大于一定数量的将军同时进攻则可以攻打成功，如果小于则进攻失败。将军中可能存在叛徒。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/be3895cd1d7b1f572bc25eeb22fa578794871ff54aff96d9ba19dd3d53da525a.png" alt="6403.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6403.png</figcaption></figure><p>这个时候有2种情况</p><p>1.如果2个叛徒都在BCDE中，那么共识算法需要让其余2个将军听从A的正确决策进攻城堡。</p><p>2.如果A是1个叛徒，共识算法需要让BCDE中剩余的3个忠诚将军保持一致。</p><p>这个问题有很多种解法，大家有兴趣可以自行查阅(推荐学习PBFT)，我们重点来看看以太坊中目前正在使用的Nakamoto 共识和将要使用的 Casper Friendly Finality Gadget共识是如何解决拜占庭将军问题的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">以太坊共识机制简述</h2><p>说到Nakamoto共识和Casper Friendly Finality Gadget共识可能大家不太熟悉，但他们的部分组成应该都比较熟悉-POW(工作量证明)和POS(权益证明)。</p><h3 id="h-sybil" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Sybil抗性</h3><p>POW或POS称之为Sybil抗性机制，为什么需要Sybil抗性机制呢，刚刚我们说到拜占庭将军问题，应该很容易看出恶意节点越多，达成正确共识的难度也就越大，Sybil攻击就是指1个攻击者可以伪装出大量节点来进行攻击，Sybil抗性是指抵御这种攻击能力。</p><p>POW通过让矿工或验证者投入算力，POS通过让验证者质押以太坊，如果攻击者要伪装多个节点攻击则必将投入大量的算力或资产，会导致攻击成本高于收益。在以太坊中保障的安全性是除非攻击者拿到整个系统51%算力或资产否则不可能进攻成功。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">最长链规则</h3><p>在解决完Sybil攻击后，通过选取系统中的最长链作为大家达成共识的链。</p><p>很多人平时为了简化将pow和pos认为是共识机制，这不够准确，但也说明了其重要作用，我们接下来分析pow和pos。</p><h2 id="h-pow" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">pow-工作量证明</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">工作量证明简述</h3><p>通过hash不可逆的特性，要求各个矿工不停地计算出某个值的hash符合某一特征，比如前多少位是000000，由于这个过程只能依赖不停的试错计算hash,所以是工作量证明。计算完成后其他节点验证的值符合hash特征非常容易验证。验证通过则成为成为合法区块(不一定是共识区块，需要在最长链中)。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">挖矿算法</h3><p>以太坊中的挖矿算法用到2个数据集，1个小数据集cache,1个大数据集DAG。这2个数据集会随着区块链中区块增多慢慢变大，初始大小cache为16M DAG为1G。</p><p>我们先来看这2个数据集的生成过程</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b9767f2780bfd7aec6b7b9e0aaad1f18b009e23e7469c11af7951d8f42690b96.png" alt="6404.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6404.png</figcaption></figure><p>cache生成规则为有1个种子随机数seed，cache中第1个元素对seed取hash，后面数组中每个元素都是前1个元素取hash获得。</p><p>DAG生成规则为 找到cache中对应的元素后 根据元素中的值计算出下次要寻找的下标，循环256次后获得cache中最终需要的元素值进行hash计算得到DAG中元素的值。</p><p>然后我们再看看矿工如何进行挖矿以及轻节点如何验证</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">矿工挖矿过程</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/49f50520017b2eec6055966b1a99325acaa57273eebe87d6894ba50435f69417.png" alt="6405.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6405.png</figcaption></figure><p>矿工挖矿的过程为，选择Nonce值映射到DAG中的1个item，通过item中的值计算出下次要找的下标，循环64次，得到最终item，将item中的值hash计算得到结果，结果和target比较，符合条件</p><p>则证明挖到区块，如果不符合则更换nonce继续挖矿。矿工在挖矿过程中需要将1G的DAG读取到内存中。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">轻节点验证过程</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/17715d490d87e5dd2672d7c6c53213384bca71fbf8d38a9ee5abe85e67538418.png" alt="6406.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6406.png</figcaption></figure><p>轻节点验证过程和矿工挖矿过程基本一致，</p><p>将块头里面的Nonce值映射到DAG中的1个item，然后通过cache数组计算出该item的值，通过item中的值计算出下次要找的下标，循环64次，得到最终item，将item中的值hash计算得到结果，结果和target比较，符合条件则验证通过。轻节点在验证过程中不需要将1G的DAG读取到内存中。每次用到DAG的item值都使用cache进行计算。</p><p>以太坊为什么需要这2个不同大小的数组进行辅助hash运算呢，直接进行hash运算会有什么问题？</p><p>如果只是进行重复计算会导致挖矿设备专业化，减少去中心化程度。因为我们日常使用的计算机内存和计算力是都需要的，如果挖矿只需要hash运算，挖矿设备则会设计地拥有超高算力，但对内存可以缩小到很小甚至没有。所以我们选用1G的大内存增加对内存访问的频率，增加挖矿设备对内存访问需求，从而更接近于我们日常使用的计算机。</p><h4 id="h-ghost" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">ghost协议</h4><p>我们看看在Nakamoto共识是如何解决拜占庭将军问题的。首先看看区块链中的拜占庭将军问题是什么？</p><p>区块链中需要达成一致的是哪条链为主链，虽然采用了最长链原则，但由于分叉问题，还是会带来拜占庭将军问题。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b5a59092d9af31840f20306e2083db815c8ec8658c91a3f8a1461399fd6ff0ff.png" alt="6407.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6407.png</figcaption></figure><p>本来以太坊pow目标是抵抗51%以下的攻击，但如上图如果恶意节点沿着自己挖出的区块不断挖矿，由于主链上有分叉存在，恶意节点不需要达到51%算力就可以超过主链进而成为新的主链，为此以太坊使用了ghost协议给上图中的B1和C1也分配出块奖励，尽快合并到主链中，这样主链长度(按照合并后的总长度算，长度只是抽象概念，以太坊中按照区块权重累加)还是大于恶意节点自己挖矿的。</p><h2 id="h-pos" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">pos-权益证明</h2><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">权益证明简述</h4><p>网络中的用户通过质押一定数量的以太坊成为验证者。每次系统从这些验证者从随机选择出区块创建者，其余验证者去验证创建出的区块是否合法。验证者会获得出块奖励，没有被选中的区块不进行验证则会被扣除一定质押币，如果进行错误验证则会被扣除全部质押币。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">权益证明过程</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6b2801cbcfd523074f04f6daec4a1fcf7481fd0b83fc1867183c946aabb2d142.png" alt="6408.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6408.png</figcaption></figure><p>如上图，权益证明在每隔一定区块的地方设置一个检查点，对前面的区块进行验证，2/3验证者通过则验证通过，验证通过则该区块所在链成为最长合法链(不能被回滚)。</p><p>我们简化地只分析了权益证明本身，在以太坊中权益证明较为复杂的点在于和分片机制结合在一起时的运行流程，这部分会在后面单独将分片机制的一篇文章中详述。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>本篇文章主要讨论了共识机制是解决分布式系统中的拜占庭将军问题，以及分析了以太坊中的共识机制一般包括最长链选择和一种sybil抗性机制(pow或pos)。重点分析了pow和pos的流程以及设计思想。</p><p>更多web3相关文章 可关注公众号 &quot;web3探索者&quot;</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f4381697c3da88942f82e34e38ab2603ab72bce1cc71026314aebc175039e6ee.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[关于区块链不可能三角的研究]]></title>
            <link>https://paragraph.com/@explorer-of-web3/CFN1jibEG7H6dImdxNEZ</link>
            <guid>CFN1jibEG7H6dImdxNEZ</guid>
            <pubDate>Sun, 03 Apr 2022 07:03:00 GMT</pubDate>
            <description><![CDATA[由于不可能三角，以太坊选择了安全性和去中心化，牺牲了交易处理速度，带来了很多拓展性的问题。区块链中的不可能三角这里的"不可能"没有任何理论论证，只是对现有区块链的总结。所以很多人也在探索突破不可能三角的方案，并已经有一定进展，后面会详细讨论。图片效率(TPS):每秒处理交易的数量。 去中心化:参与节点门槛足够低，保证系统中有大量分布式的节点。 安全:对区块链发动攻击的难度足够大。 以太坊为例我们看看不可能三角中的每一角是如何保障的。 去中心化: 以太坊中拥有大量的节点，交易被记录在链上需要全节点验证通过，所以达到了去中心化的目的。 安全性: 以太坊目前采用的POW(工作量证明机制) 对发动攻击的要求是聚集整个系统一半以上的算力。后面采用POS（权益证明）对发动攻击的要求是需要质押51%的ETH。无论哪种方式都需要付出巨额代价，并在攻击结束后，币价会大幅下降从而巨额代价无法获得回报来保证安全性。 效率：为了保证去中心化和安全性，即系统中必须拥有大量的节点，每个节点都需要执行区块中的一遍交易过程转移状态，以太坊要求每个区块中不能包含太多的交易，出块时间也要求稳定在高于一定时间之上。加...]]></description>
            <content:encoded><![CDATA[<p>由于不可能三角，以太坊选择了安全性和去中心化，牺牲了交易处理速度，带来了很多拓展性的问题。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">区块链中的不可能三角</h2><p>这里的&quot;不可能&quot;没有任何理论论证，只是对现有区块链的总结。所以很多人也在探索突破不可能三角的方案，并已经有一定进展，后面会详细讨论。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8ca79b9f58d22d64fadbbb3301bf9f8ce5582889503b255de5505b7a2ba5f366.webp" alt="图片" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">图片</figcaption></figure><p>效率(TPS):每秒处理交易的数量。</p><p>去中心化:参与节点门槛足够低，保证系统中有大量分布式的节点。</p><p>安全:对区块链发动攻击的难度足够大。</p><p>以太坊为例我们看看不可能三角中的每一角是如何保障的。</p><p>去中心化: 以太坊中拥有大量的节点，交易被记录在链上需要全节点验证通过，所以达到了去中心化的目的。</p><p>安全性: 以太坊目前采用的POW(工作量证明机制) 对发动攻击的要求是聚集整个系统一半以上的算力。后面采用POS（权益证明）对发动攻击的要求是需要质押51%的ETH。无论哪种方式都需要付出巨额代价，并在攻击结束后，币价会大幅下降从而巨额代价无法获得回报来保证安全性。</p><p>效率：为了保证去中心化和安全性，即系统中必须拥有大量的节点，每个节点都需要执行区块中的一遍交易过程转移状态，以太坊要求每个区块中不能包含太多的交易，出块时间也要求稳定在高于一定时间之上。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">加大区块容量或加速区块时间无法解决不可能三角</h4><p>将每个区块包含的交易数增大或加快出块时间，我们知道每个节点在验证区块的时候需要将区块中的交易重新执行一次，去同步状态树。无论是增加区块中的交易数量还是加快出块时间都会使得系统中性能较差的节点，无法跟上交易状态转移的速度，从而降低中心化程度。</p><h2 id="h-powpos" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">为什么POW转向POS交易速度不会得到提升？</h2><p>首先区块链中需要证明机制是一种让记账更加可信的方式。</p><p>POW(工作量证明) 是指依赖算力重复计算来获得记账权。如果经过大量计算则证明可以获得记账权，</p><p>POS(权益证明)是指依赖之前拥有的资产比例并质押一定资产来获得记账权。</p><p>以太坊POW中我们设计的挖矿难度算法是为了稳定出块时间，上面我们说到了如果出块时间过小，会导致系统中节点数量减少，降低去中心化程度。所以这里的挖矿难度是手段不是目标。从而我们知道以太坊切换到pos后为了维持系统中的大量节点也不会大幅降低出块时间。</p><h3 id="h-posgas" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">采用POS之后gas费用依旧会很高</h3><p>首先需要明确1个概念，gas费用是支付给获得记账权节点运算打包所需的费用。在POW方式中也和挖矿消耗的资源没有关系。</p><p>只要系统中节点执行程序消费能源，就会有gas费用。将POW转为POS的过程只是记账权的获取方式发生了变更，节省的是挖矿需要的电能，而不会减少系统中节点执行交易(或合约代码)的消耗，gas费用当然不会降低。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/146a7e6d6da1c27896befa714d376025e2cb88ec4ecc15f4d140a29a12370188.webp" alt="图片" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">图片</figcaption></figure><p>如上图所示，pow转pos节省的是1所消耗的能源，而3所消耗的能源也就是并不会降低，gas费用是用来支付3消耗的能源所以不会因为由pow转换到pos而降低。</p><p>那么gas费用和什么有关呢？gas费用由2部分相乘获得 gasPrice * gasUsed gasUsed执行相同的操作是固定值(代表了资源的消耗) gasPrice由发起交易方填写，gasPrice越高矿工越容易优先处理交易(代表了市场竞争)。gas费用较高很大原因是以太坊中交易处理速度较慢，所以导致交易阻塞，市场竞争激烈。想要降低gas费用还是回到需要提升以太坊的交易处理速度。</p><h2 id="h-l2" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">L2的引入是如何解决不可能三角的</h2><p>既然暂时无法提升以太坊本身的处理速度，降低其gas费用，那么有没有其他办法呢？</p><p>L2的引入就很好地解决了这一问题。既然L1的交易处理速度不好提升，那么我们就在L1的基础上搭建L2，专门提供高速的交易处理，而安全性和去中心化交由L1去保障。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/160f4364a2086c2cdfdb692aded0a387efeb4d83072c8ea5060e0f1bda10fe8d.webp" alt="图片" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">图片</figcaption></figure><p>交易在L2上快速进行，L2则按照一定的时机将最终状态同步回L1，这样交易速度得到提升，由于批次写入L1以及智能合约是在L2执行gas费用也会大幅下降。</p><p>L2由于不需要处理去中心化以及安全性，原因在于将交易或合约的执行过程与最终状态的保存分离开，过程执行放到了L2，L1只需要去保存最终的状态即可。这样大大减轻了L1的压力。但L2需要保证交易或合约执行过程中是正确性，否则写入L1的状态也是错误的，为此不同的L2也采用了很多不同的方案来保障执行过程的正确性。比如Optimistic采用了欺诈证明的方式来保障。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">以太坊的分片机制带来什么变化</h2><p>除了使用L2这种结构性调整，以太坊本身有没有办法提升TPS呢，目前比较大的进展是分片技术。\</p><p>分片机制是指以太坊中拆分出N条分片链，不同的分片链去打包不同的交易。每个节点归属于每个分片链下，只负责验证分片链上交易。由于每个节点计算量变少，出块时间会加快。分片技术带来的问题就是安全性有一些下降，因为验证1个区块的节点数量变少。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1b219e2c19cfd545569785705c1f7a6632aad5457de02b4962116ead80831e2d.webp" alt="图片" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">图片</figcaption></figure><p>这里只是简单描述分片链的机制，实际场景中分片机制会带来，跨分片交易以及智能合约交互的复杂问题，后面会找一篇文章单独讨论。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>由于不可能三角，以太坊选择了安全性和去中心化，牺牲了交易处理速度，带来了很多拓展性的问题。由POW到POS的进步不会优化交易处理速度。使用分层的思想很好地解决了在保障安全性和去中心化的情况下大幅提升交易速度。与此同时以太坊作为L1基础设施也推出了分片技术来提升交易处理速度。后续应该很多区块链应用都会建立在L2之上，而和L1的对接则会完全交给L2。</p><p>更多web3相关文章 可关注公众号 &quot;web3探索者&quot;</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/0a92db121457c934859b4051a8f0239421a4fa2df9f6b196b87dd614023e5e80.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[聊一聊智能合约 ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/BRASLAt1rwA25jVmbWE2</link>
            <guid>BRASLAt1rwA25jVmbWE2</guid>
            <pubDate>Sun, 03 Apr 2022 07:01:30 GMT</pubDate>
            <description><![CDATA[本篇文章我们简单介绍了智能合约的一些基础概念，并针对没有编程基础的同学介绍了编程中常见的概念。和大家一起分析了个简易版的智能合约帮助大家了解智能合约的整体结构。最后结合智能合约比较热门的应用NFT了解了接口的概念，再次讨论了什么是上链。 智能合约到底是什么智能合约概念从抽象概念上讲，智能合约是一种可以自动化执行的规则，现实生活中的合约，合约制定完成后需要有专门的执行角色，而智能合约将这个步骤自动化，只有满足智能合约中制定的条件就会被执行。 从具体概念上讲，我们之前有一篇文章讲以太坊的数据结构中提到每个账户都有状态树，而智能合约的目的就是通过一系列逻辑执行触发合约账户的状态变化以及通过交易改变外部账户的状态，关于合约账号和外部账户可见"以太坊账户"一文。编程语言的一些基础概念变量(广义包括常量)编程语言中，变量起着重要作用，用来存储内存数据。变量内存储的数据有各种各样的类型，有的是简单的整型，字符串，有的是较为复杂的集合类型，比如列表，映射表或上自定义的类型。变量还有1个很重要的概念就是作用域。作用域作用域代表变量的生命周期，是函数级别-函数执行完就销毁，还是对象级别，依附对象销毁...]]></description>
            <content:encoded><![CDATA[<p>本篇文章我们简单介绍了智能合约的一些基础概念，并针对没有编程基础的同学介绍了编程中常见的概念。和大家一起分析了个简易版的智能合约帮助大家了解智能合约的整体结构。最后结合智能合约比较热门的应用NFT了解了接口的概念，再次讨论了什么是上链。</p><p>智能合约到底是什么</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">智能合约概念</h2><p>从抽象概念上讲，智能合约是一种可以自动化执行的规则，现实生活中的合约，合约制定完成后需要有专门的执行角色，而智能合约将这个步骤自动化，只有满足智能合约中制定的条件就会被执行。</p><p>从具体概念上讲，我们之前有一篇文章讲<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;scene=21#wechat_redirect">以太坊的数据结构</a>中提到每个账户都有状态树，而智能合约的目的就是通过一系列逻辑执行触发合约账户的状态变化以及通过交易改变外部账户的状态，关于合约账号和外部账户可见&quot;<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483681&amp;idx=1&amp;sn=4e6ccde6bee60503086b07734e08baf0&amp;chksm=c1c9972bf6be1e3dc094c893ca748a33f13180ebe7449d70b42dcb583d390bef2080bb800ddc&amp;scene=21#wechat_redirect">以太坊账户</a>&quot;一文。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">编程语言的一些基础概念</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">变量(广义包括常量)</h3><p>编程语言中，变量起着重要作用，用来存储内存数据。变量内存储的数据有各种各样的类型，有的是简单的整型，字符串，有的是较为复杂的集合类型，比如列表，映射表或上自定义的类型。变量还有1个很重要的概念就是作用域。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">作用域</h3><p>作用域代表变量的生命周期，是函数级别-函数执行完就销毁，还是对象级别，依附对象销毁就会销毁，或又是全局级别，只有程序在运行就一直都在。在智能合约中合约中声明的变量都是存储在状态树中，所以只要合约账户存在，这个变量就存在。这一点认识非常重要，后面在讲NFT的时候会讲1个误区。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">函数</h3><p>函数代表着逻辑的执行，函数的输入有几种 1。可以访问的内存 磁盘 网络中的输入 2.调用函数者传入的参数，函数的输出也是有2种 1.可以访问的内存 磁盘 网络的输出 2.函数返回值 。</p><p>有一种纯函数的概念 即 输入只包含了调用函数者传入的参数，输出只包含了返回值。</p><p>在智能合约中，非依赖库所有能够让外部账户或者合约账户调用的函数都不应该是纯函数，否则就是对gas的浪费。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">智能合约的结构</h2><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">智能合约结构示意</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/13f7bc09838a378466b170bb77efa91a0895df775ef04cae23d92788eb896859.png" alt="640.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">640.png</figcaption></figure><p>如上图，智能合约由外部账户发起调用，合约直接也可以相互调用，每个合约内部执行函数转移状态。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">分析个例子</h2><p>例子来源:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://learnblockchain.cn/docs/solidity/solidity-by-example.html#id1">https://learnblockchain.cn/docs/solidity/solidity-by-example.html#id1</a></p><p>分析智能合约分为2块</p><p>1.合约变量，即状态每个状态代表了什么含义。</p><p>2.函数 即合约中的函数是如何运转的，又是如何影响状态变化。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>合约变量</strong></h3><pre data-type="codeBlock" text="address public chairperson;
 // 这声明了一个状态变量，为每个可能的地址存储一个 `Voter`。 
mapping(address =&gt; Voter) public voters;
// 一个 `Proposal` 结构类型的动态数组Proposal[] public proposals;
"><code><span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> chairperson;
 <span class="hljs-comment">// 这声明了一个状态变量，为每个可能的地址存储一个 `Voter`。 </span>
<span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> Voter) <span class="hljs-keyword">public</span> voters;
<span class="hljs-comment">// 一个 `Proposal` 结构类型的动态数组Proposal[] public proposals;</span>
</code></pre><p>这3个变量就是这个智能合约的状态</p><p>Address</p><p>mapping(address =&gt; Voter)</p><p>Proposal[]</p><p>表示了变量的类型 Address代表这是1个地址 mapping(address =&gt; Votor)代表是个映射表 其中key是地址 value是Voter Voter是个自定义的类型(就是一组类型的聚合)，Proposal[] 代表是个数组 数组中每个元素都是Proposal Proposal也是个自定义类型</p><pre data-type="codeBlock" text="struct Voter {        
    uint weight; // 计票的权重       
    bool voted;  // 若为真，代表该人已投票        
    address delegate; // 被委托人        
    uint vote;   // 投票提案的索引    
}
    // 提案的类型    
struct Proposal {        
    bytes32 name;   // 简称（最长32个字节）        
    uint voteCount; // 得票数
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">Voter</span> {        
    <span class="hljs-keyword">uint</span> weight; <span class="hljs-comment">// 计票的权重       </span>
    <span class="hljs-keyword">bool</span> voted;  <span class="hljs-comment">// 若为真，代表该人已投票        </span>
    <span class="hljs-keyword">address</span> delegate; <span class="hljs-comment">// 被委托人        </span>
    <span class="hljs-keyword">uint</span> vote;   <span class="hljs-comment">// 投票提案的索引    </span>
}
    <span class="hljs-comment">// 提案的类型    </span>
<span class="hljs-keyword">struct</span> <span class="hljs-title">Proposal</span> {        
    <span class="hljs-keyword">bytes32</span> name;   <span class="hljs-comment">// 简称（最长32个字节）        </span>
    <span class="hljs-keyword">uint</span> voteCount; <span class="hljs-comment">// 得票数</span>
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>合约函数</strong></h3><p>function开头的代表是一系列函数就是和前面说到的3个状态交互</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8d06cfbe316201ace197db977cf656d978ae351025eef37960a91d51a7dced19.png" alt="6401.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6401.png</figcaption></figure><p>如上图所示，智能合约中对外可调用的函数围绕着如何修改 或 读取状态而来，如同文章开始所讲到的，智能合约执行的过程就是以太坊中状态树变化的过程。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">智能合约的特点</h2><h3 id="h-gas" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>gas费用</strong></h3><p>智能合约相比于其他编程非常要注意的1点就是运行逻辑需要收取gas费用，这使得合约编写过程要额外注重代码的高效性，尽可能地减少冗余代码。</p><p>gas费用存在的原因以及gas费用为什么高昂，之前<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483691&amp;idx=1&amp;sn=9e92b314e70d813f17d7766295eb7ab4&amp;chksm=c1c99721f6be1e379cb937216fd2443583c4f4a333c399ba0b2f5039165e15071850f98961e0&amp;scene=21#wechat_redirect">不可能三角一文</a>中有讲，简单来说gas费用存在是因为计算需要耗费资源，存在gas费用可以避免恶意消耗资源的情况发生。gas费用高昂主要是因为竞争激烈。</p><p>gas费用计算可以看这里</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/zh/developers/docs/evm/opcodes%EF%BC%8C%E8%BF%99%E9%87%8C%E6%98%AF%E4%B8%80%E4%BA%9B%E6%93%8D%E4%BD%9C%E7%A0%81%EF%BC%8C%E6%88%91%E4%BB%AC%E5%86%99%E7%9A%84%E4%BB%A3%E7%A0%81%E6%9C%80%E7%BB%88%E9%83%BD%E4%BC%9A%E7%BC%96%E8%AF%91%E6%88%90%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4%E8%BF%90%E8%A1%8C%E5%9C%A8EVM%E4%B8%8A">https://ethereum.org/zh/developers/docs/evm/opcodes，这里是一些操作码，我们写的代码最终都会编译成字节码指令运行在EVM上</a>。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>围绕交易的语言设计</strong></h3><h4 id="h-payable" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>payable 关键字</strong></h4><p>这是一个函数的修饰符，只有被payable修饰过的函数才可以在调用时附带ETH。</p><h4 id="h-fallback" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>fallback函数</strong></h4><p>调用到智能合约没有的函数时会走到fallback函数, 如果没有写fallback会抛出异常，如果是涉及到附加ETH，fallback必须被payable修饰，否则也会抛出异常</p><h4 id="h-receive" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>receive函数</strong></h4><p>对合约单纯转账会进入这个函数，前提是有这个函数，如果没有会进入被payable修饰的fallback函数，否则会抛出异常。</p><p>可以看到合约语言无不透露着对交易属性，这在一般的编程语言中是不会看到的。</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>常见陷阱</strong></h4><p>合约一旦发布，即使存在漏洞就不可更改，所以对于逻辑要格外严谨。我们来看看1个常见漏洞</p><h5 id="h-" class="text-lg font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>重入攻击</strong></h5><pre data-type="codeBlock" text="//被调用的合约
contract A {        
    uint money;    /// 提取你的分成。
    function withdraw() public {
          if (payable(msg.sender).send(money))
              money = 0; 
         }
}
//调用的合约
contract B {
        function hack() public {
            A().withdraw();
       }
        fallback external payable {
        A().withdraw();
       }
}
"><code><span class="hljs-comment">//被调用的合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{        
    <span class="hljs-keyword">uint</span> money;    <span class="hljs-comment">/// 提取你的分成。</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
          <span class="hljs-keyword">if</span> (<span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">send</span>(money))
              money <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; 
         }
}
<span class="hljs-comment">//调用的合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">B</span> </span>{
        <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hack</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
            A().withdraw();
       }
        <span class="hljs-function"><span class="hljs-keyword">fallback</span> <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        A().withdraw();
       }
}
</code></pre><p>上面的代码中A给B转账会走到B的fallback函数，而B的fallback函数又调用A的转账函数，这样会导致A向B不停转账。</p><p>改进办法就是先修改状态再转账</p><pre data-type="codeBlock" text="//被调用的合约
contract A {
        uint money;    /// 提取你的分成。
        function withdraw() public {
            uint tmp_money = money; 
            money = 0;
            payable(msg.sender).send(moeny);
        }
}
"><code><span class="hljs-comment">//被调用的合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
        <span class="hljs-keyword">uint</span> money;    <span class="hljs-comment">/// 提取你的分成。</span>
        <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
            <span class="hljs-keyword">uint</span> tmp_money <span class="hljs-operator">=</span> money; 
            money <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
            <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">send</span>(moeny);
        }
}
</code></pre><h2 id="h-nft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">NFT</h2><p>NFT是最近比较热门的话题，很多人认为NFT代表了数字化的确权，也有人认为目前就是一张jpg图片，我们这里不讨论他的经济价值，讨论NFT的技术实现。在每一组NFT背后都是一份智能合约。这些智能合约都遵守着相同的规范。</p><h2 id="h-nft" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">NFT协议</h2><p>具体见:<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721">https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721</a></p><pre data-type="codeBlock" text="constructor(name_, symbol_)
balanceOf(owner)
ownerOf(tokenId)
name()
symbol()
tokenURI(tokenId)
baseURI()
tokenOfOwnerByIndex(owner, index)
totalSupply()
tokenByIndex(index)
approve(to, tokenId)
getApproved(tokenId)
setApprovalForAll(operator, approved)
isApprovedForAll(owner, operator)
transferFrom(from, to, tokenId)
safeTransferFrom(from, to, tokenId)
safeTransferFrom(from, to, tokenId, _data)
_safeTransfer(from, to, tokenId, _data)
_exists(tokenId)
_isApprovedOrOwner(spender, tokenId)
_safeMint(to, tokenId)
_safeMint(to, tokenId, _data)
_mint(to, tokenId)
_burn(tokenId)
_transfer(from, to, tokenId)
_setTokenURI(tokenId, _tokenURI)
_setBaseURI(baseURI_)
_approve(to, tokenId)
_beforeTokenTransfer(from, to, tokenId)
"><code><span class="hljs-built_in">constructor</span>(name_, symbol_)
<span class="hljs-built_in">balanceOf</span>(owner)
<span class="hljs-built_in">ownerOf</span>(tokenId)
<span class="hljs-built_in">name</span>()
<span class="hljs-built_in">symbol</span>()
<span class="hljs-built_in">tokenURI</span>(tokenId)
<span class="hljs-built_in">baseURI</span>()
<span class="hljs-built_in">tokenOfOwnerByIndex</span>(owner, index)
<span class="hljs-built_in">totalSupply</span>()
<span class="hljs-built_in">tokenByIndex</span>(index)
<span class="hljs-built_in">approve</span>(to, tokenId)
<span class="hljs-built_in">getApproved</span>(tokenId)
<span class="hljs-built_in">setApprovalForAll</span>(operator, approved)
<span class="hljs-built_in">isApprovedForAll</span>(owner, operator)
<span class="hljs-built_in">transferFrom</span>(from, to, tokenId)
<span class="hljs-built_in">safeTransferFrom</span>(from, to, tokenId)
<span class="hljs-built_in">safeTransferFrom</span>(from, to, tokenId, _data)
<span class="hljs-built_in">_safeTransfer</span>(from, to, tokenId, _data)
<span class="hljs-built_in">_exists</span>(tokenId)
<span class="hljs-built_in">_isApprovedOrOwner</span>(spender, tokenId)
<span class="hljs-built_in">_safeMint</span>(to, tokenId)
<span class="hljs-built_in">_safeMint</span>(to, tokenId, _data)
<span class="hljs-built_in">_mint</span>(to, tokenId)
<span class="hljs-built_in">_burn</span>(tokenId)
<span class="hljs-built_in">_transfer</span>(from, to, tokenId)
<span class="hljs-built_in">_setTokenURI</span>(tokenId, _tokenURI)
<span class="hljs-built_in">_setBaseURI</span>(baseURI_)
<span class="hljs-built_in">_approve</span>(to, tokenId)
<span class="hljs-built_in">_beforeTokenTransfer</span>(from, to, tokenId)
</code></pre><p>上面的函数就是NFT智能合约需要遵守的规范- ERC721。</p><p>在编程语言中有个概念-接口。之前讲过函数的概念，而接口就是一组函数声明，要实现类似的东西大家都要有个规范，于是乎就有了接口。接口不负责具体实现，只是告诉你要实现什么能力，才能够满足规范。</p><p>我们从接口中挑1个大家有过讨论的函数看看。</p><p><code>_setTokenURI(tokenId, _tokenURI)</code></p><p>这个函数目的是给这组NFT某个编号 设置一个url，这个url我们可以简单地理解为是一张图片(先简单理解)。这个接口的出现导致了，如果可以成功调用该接口，那么你高价购买到的NFT将有可能变成另外1张没有任何艺术价值的图片。</p><p>有的说法是这个url没有上链,如果上链了就不会有这个问题。所以我们接下来讨论下什么是上链。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">当我们讨论上链时，什么是上链</h2><p>很多人讨论链上的数据不可被更改，那到底什么是上链呢？在之前文章<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;scene=21#wechat_redirect">以太坊数据结构</a>中曾经提到以太坊当中的状态树，只要存在这棵状态树里面的数据都是上链了的。而智能合约中每个合约状态都是存在状态树中的，所以<strong>只要是合约中声明的变量都可以称为上链</strong> 。另外1个问题，链上数据是否不可更改呢？准确回答说按照规则就可以更改，不按照规则就不可以更改。什么是按照规则，按照规则就是如果智能合约中有对应修改这个状态的函数，满足调用函数的条件，就可以改掉它！</p><p>回到NFT url的问题，这个url肯定是存到状态树上了，毋容置疑也是上链了的，但就因为暴露了这个接口所以才导致可以被更改。</p><p>这里另外提1个NFT url篡改的风险在于同1个url内映射的文件也可能被更改，这里就需要用到不可变更的存储，如ipfs等，后面我们专门有一篇内容来分析分布式存储。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本篇文章我们简单介绍了智能合约的一些基础概念，并针对没有编程基础的同学介绍了编程中常见的概念。说到学习代码唯有2招最为有效，1.多读代码 2.多写代码 所以我们和大家一起分析了个简易版的智能合约帮助大家了解智能合约的整体结构,并分析了智能合约一些针对交易设计的特点。最后结合智能合约比较热门的应用NFT了解了接口的概念，再次讨论了什么是上链。文中我们多次涉及到之前写的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;scene=21#wechat_redirect">以太坊数据结构</a>一文，可见底层知识的重要性。</p><p>更多web3相关文章 可关注公众号 &quot;web3探索者&quot;</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/16a9eaa4d758d501801ab234ecb0f998f62a275a4bce4ce110c86fc89f28e6c3.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[去中心化存储的那些事(上) ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/YbFj9ZtjSrGveLnfVZ9V</link>
            <guid>YbFj9ZtjSrGveLnfVZ9V</guid>
            <pubDate>Sun, 03 Apr 2022 06:59:06 GMT</pubDate>
            <description><![CDATA[介绍了ipfs和​filecoin各自的工作原理以及解决的问题。ipfs解决了去中心化存储的分发问题，filecoin解决了去中心化存储的存储问题​。两者并非竞争关系而是互补关系，要​进行结合使用。​为什么需要分布式存储中心化存储有着内容的访问权利过于集中,依赖中心化服务，中心化服务集群可能故障，导致不可用,重复下载内容多，速度较慢,文件存储时间短等问题，我们知道区块链是去中心化的，但以太坊等主流区块链只能在链上存储较小数据，所以对数据量较大的存储无能为力，所以去中心存储需要解决方案。 我们将介绍去中心化相关的3种技术ipfs，filecoin，arweave。本文先介绍pfs和filecoin，arweave将在下篇文章中介绍。IPFSIPFS简介ipfs有着多重含义，既是文件系统，也是一种传输协议。本文将重点解释器作为传输协议的部分，可以理解其对比对象为http协议。 ipfs可以实现数据内容从多个分布式节点获取，将大的内容切块，使用内容寻址减少重复下载。 我们可以看看获取一份内容，由http变为ipfs的过程有何不同(不包含路由阶段)。6409.png如上图所示，在传统ht...]]></description>
            <content:encoded><![CDATA[<p>介绍了ipfs和​filecoin各自的工作原理以及解决的问题。ipfs解决了去中心化存储的分发问题，filecoin解决了去中心化存储的存储问题​。两者并非竞争关系而是互补关系，要​进行结合使用。​</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">为什么需要分布式存储</h2><p>中心化存储有着内容的访问权利过于集中,依赖中心化服务，中心化服务集群可能故障，导致不可用,重复下载内容多，速度较慢,文件存储时间短等问题，我们知道区块链是去中心化的，但以太坊等主流区块链只能在链上存储较小数据，所以对数据量较大的存储无能为力，所以去中心存储需要解决方案。</p><p>我们将介绍去中心化相关的3种技术ipfs，filecoin，arweave。本文先介绍pfs和filecoin，arweave将在下篇文章中介绍。</p><h2 id="h-ipfs" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">IPFS</h2><h2 id="h-ipfs" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">IPFS简介</h2><p>ipfs有着多重含义，既是文件系统，也是一种传输协议。本文将重点解释器作为传输协议的部分，可以理解其对比对象为http协议。</p><p>ipfs可以实现数据内容从多个分布式节点获取，将大的内容切块，使用内容寻址减少重复下载。</p><p>我们可以看看获取一份内容，由http变为ipfs的过程有何不同(不包含路由阶段)。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d3e938cb1d443bc6131beb7677efa66981fcb9018ca022841d95778b6d36cd07.png" alt="6409.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6409.png</figcaption></figure><p>如上图所示，在传统http中我们向单点服务器请求数据dataA,中心化服务器需要完整返回dataA，而在ipfs中会将dataA进行拆分，拆分成几个较小数据集然后向不同节点(就近原则)分别请求这些data,如果节点已有的数据则不需要请求。</p><p>上面我们描述ipfs获取数据过程中有2个问题</p><p>1.ipfs为什么能够根据内容本身去寻找数据提供者(在http可不是这样, 是根据位置标识符来寻找的)。</p><p>2.客户端(需要获取数据的节点) 是如何找到拥有对应数据的节点。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">内容寻址</h3><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">内容寻址简介</h4><p>内容寻址即根据内容去寻找它所处的位置，我们平时使用的http在浏览器中输入url都属于通过位置去找内容。</p><p>通过内容寻址有什么好处呢？当我们访问网络上资源时，如果使用内容寻址这一资源不会发生变化，<strong>如果通过地址寻找内容时，可能会出现url未发生变化但资源已经变成另外1个。这也是为什么目前较火的nft很多使用ipfs存储图片信息，如果存储在http服务器中很可能一张高价nft图片过几天就变成另外1张廉价图片。注意:并不意味着存储在ipfs就不会发生更换图片的事件，nft协议有提供更换资源地址接口，但调用合约是链上操作会留下记录。</strong></p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">内容寻址的实现</h4><p>在ipfs中我们针对每块内容进行hash获得1个cid，这个cid就可以表示这一内容，如果内容发生了变化cid也一定会变化。</p><p>那么ipfs中是不是内容每更新一次我们就需要重新换个地址寻找呢，这部分内容可以查阅ipns，本文不详细展开。</p><p>在ipfs中针对文件夹这种形式还做了特殊的支持，我们先看看我们常见的文件夹格式是怎么样的。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/00ba5dc8bfa1a91ccd5d419c4719d181d3e930b8d1bf1961b8295117c68e1eeb.png" alt="6402.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6402.png</figcaption></figure><p>如上图所示，根目录T1 中包含2个文件夹T2 T3和1个文件D, T2文件夹中包含2个文件A B T3文件夹中包含 文件C。</p><p>ipfs使用merkleDAG来组织文件夹形式, merkle tree的结构之前的文章<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483679&amp;idx=1&amp;sn=ad595ee13b0c85b1ce6f2cf11ba8df2e&amp;chksm=c1c99715f6be1e03e21cc83c36cf4e2d395416d36294b23a828cffd3140be0cec276922f4aab&amp;scene=21#wechat_redirect">以太坊数据结构</a>已经详细介绍过，merkleDAG并无太大区别，区别只在于非叶子节点可以存储数据。我们看看上面的文件夹是如何组成merkleDAG的。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/26a8b8198a70489365be4c662522a260ca3881ed1e92df841c24370a2e7aa413.png" alt="6403.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6403.png</figcaption></figure><p>这样如果目录中的任何1个文件变化，都会影响其父节点hash值的变化从而影响整个目录的hash值。</p><p>上面我们回答了第1个问题，为什么ipfs能够进行内容寻址，接下来我们再看看第2个问题如何找到对应数据节点的，也就是如何进行寻址。</p><h3 id="h-dht" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">DHT(分布式哈希表)</h3><h4 id="h-dht" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">DHT简介</h4><p>我们直观去想如何在得知内容唯一标识去寻找存储数据的节点。</p><p>应该是有1个路由表去存储以内容标识为key，节点信息为value。那这个路由存在哪里呢？最简单的肯定就是存在某个服务器上，也的确有一些p2p是这样的设计，但这与去中心化的模式又有所相悖。也会带来中心化服务器的宕机风险，和访问权限过于集中的问题。</p><p>ipfs使用了DHT的方案，目标是希望将集中式地维护hash表变为由网络中的节点各存储hash表的一部分，这样可以解决中心化的风险。</p><p>DHT将key的集合拆分成多个子集，每个节点按照一定规则存储一部分子集，中心化的存储路由表到DHT的变化如下图所示。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7ccb8d857359e9bf7042cd47eafc0597c4098c437691577eab38fbf143901c28.png" alt="6404.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6404.png</figcaption></figure><h4 id="h-dht" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">DHT实现</h4><p>DHT的解决方案会有1个非常大的挑战，映射CID到存储节点的路由表是分布在网络中的各个子节点的，那么当需要获取某个CID内容的时候第一步如何进行呢？总不能无规律遍历网络中所有节点并遍历其中存储的哈希表，这样效率会非常低。</p><p>在ipfs中为每个节点引入了PeerID，注意这个PeerID和cid位数一致。这样每个PeerID就会存储其对应一组cid和存储着其每个数据块节点的映射表。也就是说通过cid就可以计算出存储其路由表的PeerID。</p><p>这就为什么从ipfs网络中获取数据需要进行2次查询，第1次的寻找路由表所在节点 第2次寻找存储数据块的节点。</p><p>ipfs中获取数据块的完整过程如下</p><pre data-type="codeBlock" text="1.将待获取的数据块cid 简单计算得到其存储其哈希表的节点PeerID1

2.在网络中查找PeerID1

3.在PeerID1的哈希表中获取存储cid对应数据块的节点PeerID2

4.在网络中查找PeerID2

5.在PeerID2中找到cid对应的数据块，使用数据交换协议进行下载
"><code></code></pre><p>此外ipfs在访问数据过程中使用距离(路由跳数)来选择较近节点优先访问较近的节点，具体实现不再赘述，有兴趣可以自行查阅。</p><p>上面描述的ipfs看上去好处多多，但实际过程中ipfs遇到一个问题，ipfs只解决了去中心化的高效分发的问题，但没有解决存储的问题。 ipfs中存储不能依赖网络中的其他人，提供的是&quot;我需要的数据你正好拥有那么我来你这里拿”的协议，协议并未解决将数据持久化存储。所以内容要么本地存储，要么存储在ipfs固定存储服务中。</p><p>那么去中心化存储如何解决呢？接下来我们看看filecoin的解决之道。</p><h2 id="h-filecoin" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">filecoin</h2><h2 id="h-filecoin" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">filecoin简介</h2><p>上面说到ipfs只解决了去中心化高效分发，但未解决去中心化存储，而filecoin专为去中心化存储而设计。</p><p>filecoin是一种区块链的设计，具备区块链的大部分特征。所以分析filecoin可以参考之前的以太坊技术相关的几篇文章。</p><p>如下图所示，filecoin通过自己的代币FIL和存储提供者提供存储空间进行交易记录到链上，从而形成了一个自由的存储市场。</p><p>filecoin核心难点是数据是在链下存储的，而链下操作是无记录不可追踪的，那么如何向链上证明的确存储了客户端数据呢？</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">复制证明</h2><p>存储提供商将存储的数据进行编码生成数据副本，然后将数据副本的merkle tree的根hash连同自己的身份标识和存储时间进行hash，获得自己存储了这个数据的证明。这个过程称为密封，证明经过压缩后上传到链上。</p><p>使用复制证明可以验证存储提供商上链前的确存储了数据，但因为存储是一个持续的过程，如果存储提供商在证明提交上链后删除本地存储的数据怎么办呢？filecoin使用时空证明来解决这一问题。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">时空证明</h2><p>filecoin在存储期限内，会向存储提供商提出挑战，进行随机访问，存储提供商需要证明自己知道这些随机访问的数据。如何证明呢？这里又用到我们经常提到的merkle树，刚刚论述复制证明时将数据进行编码变成一棵merkle树，给定随机访问的节点数据，如果可以给到能够组成merkle树相同根节点的证明即可。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/950e6eefc6217af919ce9568d00f33e3c6d083d8e2d362de5b3534bf803c1354.png" alt="6406.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6406.png</figcaption></figure><p>如上图所示，随机访问红色节点，需要给出绿色节点的证明即可。</p><p>同时还需要给出根节点的证明,因为链式并不直接存储根节点，链上验证根节点正确，并且绿色节点和随机访问的节点可以组成给出的根节点则证明成功。</p><p>在filecoin中存储提供商需要提供一定的质押，如果时空证明失败，质押将会被扣除。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">共识机制</h2><p>前面一篇文章<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483704&amp;idx=1&amp;sn=8421aad1400b24aa38ec06a160220c03&amp;chksm=c1c99732f6be1e24fa8358e898fba6e73043688760b7feffd2c27d3008cfa71722f8ad1cad4b&amp;scene=21#wechat_redirect">以太坊共识机制</a>介绍过，分布式系统是需要共识机制的，那么filecoin的共识机制是什么呢？</p><p>filecoin的共识机制称为EC共识和以太坊切换成pos后的共识机制类似，都是选举节点生成区块，只不过以太坊中的权益体现在质押的ETH，而filecoin中的权益体现在提供的存储，提供的存储越多被选中生成区块的概率也就越大。</p><p>filecoin使用复制证明和时空证明达到链下存储数据的目标，为区块链中存储较大数据提供了一种思路。</p><p>filecoin和ipfs</p><p>首先 filecoin和ipfs不是竞争关系，而是互补关系。如上文所述，ipfs有着优秀的传输分发机制，但并没有解决存储的问题，filecoin本就为存储而生，希望能够高效利用其大量空置的存储并形成全新的存储市场。</p><p>filecoin也是基于ipfs架构，节点类型也都属于ipfs节点，只是在构建ipfs DHC的时候filecoin不参与，在filecoin中寻找数据，filecoin提供了检索交易的方式但比较麻烦。所以很多架构都是二者结合使用，将数据存在ipfs中方便分发，同时也向filecoin存储一份作为冗余备份。</p><p>如下图所示,存储数据时先到ipfs查询，找到则直接返回，没有找到则在filecoin中进行检索交易。存储数据时共享数据到ipfs网络中，并进行存储交易备份到filecoin中。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8ff933f868e7f182e890e9765fe9a9c5c615b1b7f3c99a68c6271da25493d490.png" alt="6407.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">6407.png</figcaption></figure><p>filecoin虽然提供了分布式存储，但本身并未实现存储冗余，让我们试想一下，如果你将数据存储到filecoin中的某个存储提供商中后，如果这个存储提供商机器故障存储丢失了，那么除了扣除这个存储提供商质押之外并无其他办法。而arweave从机制上避免了此类问题，实现了永久存储，下一篇文章我们将介绍arweave相关。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结尾</h2><p>本文从中心化存储的缺点开始论述，介绍了ipfs和filecoin各自的工作原理以及解决的问题。ipfs解决了去中心化存储的分发问题，filecoin解决了去中心化存储的存储问题。两者并非竞争关系而是互补关系，要进行结合使用。最后提到filecoin是去中心化存储方式，但未从技术方案上避免解决存储丢失的问题。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/8997f3b1a5cf94b86288acaab2b57589a6d69bc0ec712d8c2c62f19582d307c6.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flow白皮书1-共识和计算分离(译) ]]></title>
            <link>https://paragraph.com/@explorer-of-web3/flow-1</link>
            <guid>dcllpNX4pOCYifcdLlg5</guid>
            <pubDate>Sun, 03 Apr 2022 06:54:26 GMT</pubDate>
            <description><![CDATA[本文翻译自Flow白皮书1-共识和计算分离，不带有个人观点。也不构成任何投资建议。摘要现有区块链架构已经被证明吞吐量是受限的，这是其被广泛采用的最大障碍之一。为了应对这一挑战在尝试侧链或Layer2解决方案，包括比特币的闪电网络或以太坊的Plasma网络，它们可以脱离主链工作。解决这个问题的另一技术是分片，即将网络分解为许多相互连接的网络。但是这些扩容方法打破了ACID(原子性、一致性、隔离和持久性)，增加了应用程序开发的成本和时间。 在本文中，我们介绍一种新颖的方法，我们将矿工分为两个不同的节点类型。具体来说，选择、排序工作和执行工作将相互独立。本文的重点将介绍共识和计算分离，并证明这种方法增加了吞吐量但不影响安全性。 与大多数现有方案相比，我们的方法通过分离共识和计算来实现扩展，即更好地利用资源。这种方法使智能合约的编程范式(事务操作原子性)得以保持并且未增加额外复杂度。我们进行模拟网络试验，试验采用相同的共识算法(具有轮换区块提议者的两步提交协议)，区块计算要么在共识节点执行(传统架构)或委托给专门的执行节点(共识和计算分离架构)。共识和计算分离使我们的系统与传统系统相比，...]]></description>
            <content:encoded><![CDATA[<p>本文翻译自Flow白皮书1-共识和计算分离，不带有个人观点。也不构成任何投资建议。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">摘要</h3><p>现有区块链架构已经被证明吞吐量是受限的，这是其被广泛采用的最大障碍之一。为了应对这一挑战在尝试侧链或Layer2解决方案，包括比特币的闪电网络或以太坊的Plasma网络，它们可以脱离主链工作。解决这个问题的另一技术是分片，即将网络分解为许多相互连接的网络。但是这些扩容方法打破了ACID(原子性、一致性、隔离和持久性)，增加了应用程序开发的成本和时间。 在本文中，我们介绍一种新颖的方法，我们将矿工分为两个不同的节点类型。具体来说，选择、排序工作和执行工作将相互独立。本文的重点将介绍共识和计算分离，并证明这种方法增加了吞吐量但不影响安全性。 与大多数现有方案相比，我们的方法通过分离共识和计算来实现扩展，即更好地利用资源。这种方法使智能合约的编程范式(事务操作原子性)得以保持并且未增加额外复杂度。我们进行模拟网络试验，试验采用相同的共识算法(具有轮换区块提议者的两步提交协议)，区块计算要么在共识节点执行(传统架构)或委托给专门的执行节点(共识和计算分离架构)。共识和计算分离使我们的系统与传统系统相比，在不损失安全性和去中心的情况下吞吐量增加了56倍。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">内容</h3><ol><li><p>介绍</p></li></ol><ul><li><p>1.1 相关工作</p></li></ul><ol><li><p>架构概述</p></li><li><p>理论性能和安全分析</p></li></ol><ul><li><p>3.1 共识节点的特殊作用</p></li></ul><ol><li><p>进一步的工作</p></li><li><p>结论</p></li></ol><p>最初的比特币区块链使用称为Nakamoto共识的共识模型。它使用顺序模型，在该模型中节点构建、挖掘和验证一个块，节点们在此之上构建后续的块。虽然POW保障了区块链链的安全性，但相应的计算工作量限制了出块速率和交易吞吐量。这种架构已经被证明吞吐量是受限的，这是其被广泛采用的最大障碍之一。 消除工作量证明开销的主要方案是采用拜占庭容错(BFT) 共识算法。使用POW 的区块链需要巨大的计算量以及电能。相比之下，通过BFT确定区块共识是高效的，但需要一组绝大多数诚实的参与者。 将 BFT 共识与权益证明 (PoS)相结合，可以创建具有强大安全属性网络。在 PoS 下，所有参与节点都必须质押权益(token)，如果违反协议规则，可以将其扣除。赋予每个节点的影响力与其质押数量成正比。此外，存入的权益具有防止 Sybil 攻击的额外好处。 PoS增加链的吞吐量，降低保证链安全性的开销。但即使采用POS增加性能，吞吐量的限制仍然是区块链被广泛采用的障碍。 在本文中，我们探索了一种提升PoS区块链吞吐量的新方法。尽管PoS区块链方案消除了工作量证明，但他们倾向于从上一代的工作量证明系统继承大部分架构。特别是，网络中的每个全节点都需要检查和执行每个提议的区块并更新他们的本地副本以保持状态一致。因为每笔交易都需要被所有全节点处理，向系统添加节点对吞吐量没有好处。反而，添加节点会降低大多数BFT共识协议的吞吐量，因为最终确定一个块的消息复杂度会随着共识节点的数量超线性增加（参见第3.1 了解详情）。因此，大多数 PoS 区块链必须在一个小的共识委员会（削弱安全性）或低出块率（降低吞吐量）之间权衡。 对于不愿妥协安全性或去中心化的网络，解决扩展问题的最常见方法是通过分片或将工作从主 链移走（例如Lightning或 Plasma网络）。不幸的是，这两种方法都将对访问分布在不同地方的状态产生限制。这些限制极大地增加了应用开发者(基于智能合约)的复杂度。 我们提议的Flow 架构通过从根本上改变区块链。 Flow 将交易的选择和排序与其执行分离 这样两个进程就可以并行运行。解耦可以显著提高交易量吞吐量(高于其他区块链架构)，而不会破坏安全性或去中心程度。 传统的区块链架构需要对每个区块状态的更新结果进行确认，作为共识过程的一部分。因此，每个节点都必须在区块确定前进行状态更新计算。我们发现确定区块中交易顺序时才需要共识。一旦该顺序确定，计算结果是确定的。因此，共识部分的计算量将显著减少。一旦交易顺序确定后，就可以委托后续进程执行计算，而不影响系统的去中心化。 我们的主要工作是第3部分，在那里我们讨论和证明我们的中心定理，即可以在不影响安全性的情况下将大部分计算和通信负载与共识分开。 本文的重点是介绍共识和计算的分离，并证明这种方法可以增加吞吐量，同时具备强大的安全保证。围绕本文介绍设计的区块链需要指定各种附加机制包含：</p><ul><li><p>用于验证计算结果的完整方案。</p></li><li><p>共识算法的细节。</p></li><li><p>完善的奖励机制和惩罚机制来激励节点遵守规则。 这些主题的介绍和分析留给后续文章。</p></li></ul><h2 id="h-11" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1.1 相关工作</h2><p>图灵完备的区块链通常会对一个区块内的计算施加上限，例如以太坊的gas限制。这样的gas限制反过来又引入了吞吐量限制。施加gas限制的一个原因是为了避免验证者的困境。通过将gas限制设置得足够低，验证的时间投入与完成POW中设定的挖掘区块挑战相比，可以忽略不计。因此，gas限制确保执行验证工作不会给节点成功挖掘下一个区块带来影响。 对于PoS区块链，验证者的困境仍然存在。特别是对于高吞吐量的区块链，大量交易的验证会消耗大量的计算资源和时间。通过分离共识和计算，共识节点不再有验证计算结果正确性的验证者困境。块中的最大计算量（块的gas限制）不会受到共识节点的限制。但仍然需要为验证者解决计算问题。解决方案包括zkSnarks、Bulletproofs和TrueBit。对于 Flow，我们开发了专门的知识证明 (SPoCK) 来解决验证者的困境，在后续的文章中有详细描述。 将事务排序问题与计算结果的工作分开的概念之前已经在分布式数据库领域提出过。事务通过共识排序到日志中，每个节点可以独立处理事务。然而，这些系统设计用于维护良好的数据中心，无需担心拜占庭攻击。 在区块链领域，Ekiden论文描述了一个共识与计算分离的系统，其目标是保护隐私。该论文解释了Oasis区块链技术的一部分，并指出这种方法可以提高性能。但它没有量化性能提升效果以及没有证明这种方法构建的系统将能够保证安全性。</p><h2 id="h-2" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2 架构概述</h2><p>Flow 架构建立在计算和共识分离的原则之上。网络中有专门角色：共识节点和执行节点。这2种节点类型的核心区别是解决客观问题还是主观问题。客观问题是那些有客观的正确答案。任何传统的数学计算都是客观的；你不需要权威或专家确认2+ 2 = 4的正确性。主观问题没有这样的确定性解决方案。大多数人类治理系统（“法律”）通常处理主观问题。在不同社会的不同时期，有关规则可能会非常不同。共识一词的定义意味着在主观问题上达成一致，没有单一的正确答案。 传统的区块链架构要求参与网络的节点同时解决主观问题和客观问题。在 Flow 中，共识节点负责处理所有主观问题，执行节点仅负责完全确定的、客观的问题。我们简要介绍共识节点和执行节点。</p><p>共识节点 共识节点通过交易数据形成区块。许多节点就提议区块达成一致需要拜占庭容错(BFT)共识算法，本文的结论适用于任何具有确定终局性的BFT共识算法。 在Flow中，一个块引用它包含的交易并定义它们的执行顺序。但不包含块中交易执行完成后的状态。因此，共识节点不需要维护计算状态或执行交易。 此外，共识节点裁定来自其他节点的惩罚请求，例如执行节点产生了错误的输出。</p><p>执行节点 当按照共识节点确定的顺序执行时，执行节点提供确定交易结果所需的原始计算能力。当执行节点的输出结果被验证是错误的时候可以要求惩罚他们。一旦它们被验证是正确的，他们将为区块链当前的状态提供证明。验证过程——错误的结果被拒绝(会惩罚产生错误结果的执行节点)，正确的结果被接受(在网络中被共享)。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1c0b4c44204d922c72c390e412ef55e32246dd9f9927121a283c2a0256b5c300.png" alt="48ccb968e505463eb4fe2b4c637d2213.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">48ccb968e505463eb4fe2b4c637d2213.png</figcaption></figure><p>图 1：通过共识和执行节点的消息流概览。为简洁起见，仅显示正常操作期间的消息。惩罚请求被裁定期间交换的消息被省略</p><h2 id="h-3" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3 理论性能和安全分析</h2><p>在本节中我们将进行理论推导，证明可以在不影响安全性的情况下将大部分计算和网络通信从共识节点分离。 Flow的设计确保在由执行节点引入的错误会具备四个关键属性: 可检测的：根据定义确定性过程具有客观正确的输出。因此，即使是网络中只有单个诚实节点也可以检测到确定性错误，并通过指出执行错误的部分来向其他诚实节点证明这个错误。 可追溯的：Flow 中所有确定性的输出必须被生成这个输出的节点进行身份签名。因此，已检测到的任何错误可以清楚地追溯到负责该输出的节点。 可惩罚的：Flow网络中的所有节点，包括执行节点和共识节点，必须进行权益(token)质押，如果被发现具有不利于正确共识达成或输出错误结果的行为，将对这些节点进行权益(token)扣除(因为每个行为都是可检测和可追溯的)。 可恢复：系统必须具有在检测到错误时撤销错误的方法。这将阻止节点进行收益大于被惩罚的恶意行为。 这样的设计一个重要特征是对于系统内部的每个操作都可以找到对应参与者负责。具体来说，除了共识节点之外的操作，从节点中选择一个子集负责执行，另外一个不相交的子集负责验证结果。下面简单介绍工作过程:</p><ul><li><p>执行组和验证组都是随机选择的。选择的节点使用可验证的随机函数，这样结果是确定性的。</p></li><li><p>任一组中节点的被包含的概率与其质押的权益(token)成正比。这要求节点必须锁定大量权益(token)，才可能具备影响系统的可能性。这会强化系统对女巫攻击的抵抗力。</p></li><li><p>两个组内节点要求质押的权益(token)要足够高，这样每个组分到全是恶意节点的概率就非常小。</p></li><li><p>只要任一组中至少有一个诚实节点，诚实节点就会检测到错误并标记。</p></li><li><p>如果标记了错误，共识节点将进行裁定，确定是错误后，恶意节点将被惩罚，操作结果将被拒绝。</p></li></ul><p>上述过程保证了恶意执行节点几乎可以肯定被惩罚。此外，恶意节点几乎不可能成功引入错误。 可能出现的一个问题是，为什么 Flow 具有独立的执行者组和验证者组而不是执行者来验证彼此的结果。我们通过这种分离解决验证者的困境。如果没有专门的验证者角色，执行者计算下一个块与验证（重新计算）之前块的结果之间存在利益冲突。在 Flow 中，这种困境通过专门的验证者得到缓解。关于验证协议的技术细节将在后续文章中介绍，其中包括了如何解决验证者不进行计算批准任何结果或验证者恶意标记正确的结果导致网络阻塞等问题。 以下定理介绍了Flow架构的安全保证并证明通过发布或批准错误结果将错误引入系统是经济的不可行。 定理 1（将工作委托给执行组和验证组从攻击成功的概率上看是安全的） 如果按照以下条件约束，则通过故意发布错误结果或验证通过错误结果将错误引入系统在经济上是不可行的。</p><ol><li><p>操作委托给两组随机选择的节点：</p></li></ol><ul><li><p>(a) 一组执行者：该组的成员执行操作。</p></li><li><p>(b) 一组验证者：该组的成员验证执行的结果。</p></li></ul><ol><li><p>只要两组同时都选中全部是恶意节点的概率足够低，两个组的节点规模可以相对较小。</p></li><li><p>执行组在生成执行结果时，验证者组的节点对他们来说是不可知的。</p></li><li><p>共识节点</p></li></ol><ul><li><p>(a) 绝大多数节点对公布的结果做出了承诺，并且参与节点无异议</p></li><li><p>(b) 或裁定异议，确定故障节点（可归因），并惩罚它们。</p></li></ul><p>需要强调的是，共识节点只需要检查是否有足够的节点执行并对其验证就足够了,他们不需要检测结果本身。定理1适用于任何具有确定终局性的BFT共识算法。</p><p>定理 1 的证明： 我们将证明定理1在符合以下条件时总是可以满足</p><ul><li><p>两组中至少有一组从由诚实节点占据多数的大量节点抽取的；</p></li><li><p>恶意节点不能抑制诚实节点之间的通信，即，如果有一个诚实节点反对结果并证明其错误，错误节点将被惩罚。</p></li></ul><p>具体来说，让我们考虑一组N个节点，我们希望从中随机抽取一个具有 n 个节点的子集。此外，我们假设最多有 M &lt; N/3 个恶意节点。在下文中，我们将重点关注所有节点质押权益(token)均等的情况，即它们被包含概率是相同的。选中 m ≤ n 个恶意节点的概率由下式给出超几何分布</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b0a7341b5676f2254429221e8a6f0c143a4becfea9e232ff0c078c6d673a5bc0.png" alt="be39e0d41d9a43fa93ae429367a62c63.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">be39e0d41d9a43fa93ae429367a62c63.png</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6b8df09938c42d0dc3433353073b34b3d9167dfc9379f22a027ac35dfbc47179.png" alt="c96e1658d156445480320bf3527389e4.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">c96e1658d156445480320bf3527389e4.png</figcaption></figure><p>其中 Pn,N,M (n) 是正好全部选中恶意节点的概率</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/76ceb70570dab5047b1b3d02595815302b01e68541d8fd3f98e56e51b2656e55.png" alt="e914055c695c46e091a58211f311872e.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">e914055c695c46e091a58211f311872e.png</figcaption></figure><p>(译者注:这里的存在一些公式，简单推导下 Pn,N,M(m)=C(m,M)C(n-m, N-M)/C(n, N),当m=n=k时 Pk,N,M=C(k,M)/C(k,N)=(M!/k!* (M-k)!)/(N!/k! *(N-k)!)= M!/N! <em>(N-k)!/(M-k)! 这里的C(m,n)是大家都学习过的求组合方式，C(m,n)=n!/m!</em> (n-m)!)</p><p>上式(4)表明，仅选中恶意节点的概率随着n的增加而严格单调递减。也就是说样本量n越大，只选中恶意节点概率越小。</p><p>节点通过发布错误结果或批准错误结果来故意攻击网络，我们假设存在奖励r，如果它的攻击成功，节点会收到这些奖励r。但是如果攻击被发现，该节点将被惩罚ξ。 攻击网络所产生的期望收入为</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4054842119a40882ec9745fe2b3a1dd652abac3597f2757c1e072231cbc9c77d.png" alt="97982c5d2bd341fd828fca287a25cb44.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">97982c5d2bd341fd828fca287a25cb44.png</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8576fa1e0358335643bcf67cb8440ec27163da7076b8c0a9b415c98f58fff751.png" alt="f1697ffbb8144a12823471ad6c581f1e.png" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">f1697ffbb8144a12823471ad6c581f1e.png</figcaption></figure><p>此外，式(4) 意味着 r/ξ 随着 n 的增加严格单调增加。当 n &lt; N/2 时，r/ξ 随 n 呈指数增长。 式 (7) 的左侧 r/ξ 是安全性度量，因为它代表攻击系统的成本。举个例子，让我们考虑 N = 1000，M = 333，n = 10 的情况。为简单起见，假设对于发布或批准错误结果，该节点质押的全部权益(token)将被扣除。然后，为了攻击在经济上可行，成功奖励 r 需要是节点权益的 65343倍(译者注:由式7计算得来Pn,N,M=M!/N!*(N-n)!/(M-n)!=333!/1000!<em>990!/323!=(333</em>...324)/(1000 *... 991) r/ξ=(1000 <em>... 991)/(333</em>...324) - 1= 65343)。如果验证者每人质押 1000 美元，攻击者平均需要花费6530万美元覆盖质押被扣除的成本。当进一步增加n到 n = 20，攻击者需要花费 r/ξ = 5.2 * 10^9 倍质押权益(token)来发布错误结果。</p><p>总之，我们分析了将执行和验证委托给1个较多节点子集的结果。证明了通过发布或批准错误结果将错误引入系统在经济上是不可行的。这个结果适用于除共识节点外的其他节点。因此，共识节点只需要检查是否有足够的节点执行并对其验证就足够了,他们不需要检测结果本身。</p><p>定理1很关键，因为它允许我们：</p><ul><li><p>将大部分计算和通信负载与共识分开；</p></li><li><p>根据不同的需求研制专业化的节点(而不是要求节点在所有方面都具有优秀的性能，达不到就降低网络吞吐量)</p></li></ul><p>其他节点以小组的形式相互验证，而共识节点以及共识委员会内部完成验证。</p><h2 id="h-31" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3.1 共识节点的特殊作用</h2><p>共识节点通过 BFT 共识算法确定事件的相对顺序。我们的结论适用于任何具有确定终局性的 BFT 共识算法，例如:HotStuff、Casper CBC或 Fantômette。</p><p>不同于其他节点的操作，共识节点在没有外部验证的情况下生成区块需要共识委员会的通过。区块的内容会被验证，如果发布的区块包含无效内容，共识节点将会被惩罚，但与其他操作不同(译者注：执行或验证)的是区块不会被重建。发布后外部各方可以检查最终确定的区块。但是，如果进行分叉攻击，双花攻击此时可能已经成功。为了提高整个系统的可恢复性，共识节点委员会应由尽可能多的质押节点组成。</p><p>对于一个简单的 BFT 算法，每个块的消息复杂度 η（所有 N 个节点发送的消息）为 O(N^2) 。更高级的协议实现O(Nlog N)或 O(c*N)，c是常量。整个共识委员会的总带宽负载 B[MB/s]为 B = β *b * η β是整体网络阻塞率, b为消息大小 MB/s。 对于接收消息m并处理它的节点，增加延迟 L(m) = f(b) + process(m) 其中 f 表示接收 m 的网络传输时间， process(m) 表示处理 m 的计算时间。很明显，B 和 L 都很大程度影响共识算法的吞吐量。这高度依赖于选择的 BFT 协议，以及拓扑结构。其他因素包括延迟或带宽限制在底层网络硬件中。</p><p>目前，我们的目标是数千个节点的共识委员会。为了去中心化，应该是使得个人仍然可以运行节点并参与共识。因此，给定所需的网络阻塞率β和环境确定的函数 f，只有减小消息大小 b 或 process(m) 才能增加共识委员会中节点数量。</p><p>我们简要介绍如何同时减少 b 和 process(m)。各个过程的详细机制，请读者参考后续文章。</p><ul><li><p>我们将计算委托给专门的执行节点。消除了共识节点维护和计算状态的需要，这显著减小了process(m)。</p></li><li><p>共识节点在正常运行期间不需要完整的交易数据。专门的收集器节点从外部客户端接收交易并将它们预打包成为集合。共识节点仅对集合引用（哈希）进行排序，这与处理单个交易数据相比，大大减少了消息大小b。</p></li></ul><p>我们通过将我们的方法与 Vlad Zamfir提出的三角(译者注:这里三角是针对POS系统，因为要进行BFT投票所以低最终确定性延迟、低网络通信开销、多节点数量不能够同时满足，需要进行平衡)进行比较来结束本节(图2)。三角形表达了Zamfir对于POS系统的猜想。Flow通过将共识节点和执行节点进行拆分进行优化</p><ul><li><p>共识节点作为共识委员会。为了确保最终区块的安全性和快速生成，我们接受更高的通信开销（三角形左下角的角）。然而，不同于其他区块链，这种共识只决定一个区块内的交易顺序，而不是结果状态。该架构通过使用集合引用而不是完整的单个交易。进一步提高吞吐量并减少通信延迟，所有可能的计算都委托给其他节点类型。</p></li><li><p>执行节点将解包并处理单个事务。在不涉及大量恶意节点情况下，收集器节点可以和执行节点直接完成无需共识节点。此外，任务可以由小组进行并行化处理，小组相互负责以确保正确的结果。只有恶意攻击期间，恶意行为才会报告给共识节点以进行裁定惩罚。</p></li></ul><h2 id="h-4" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4 进一步的工作</h2><p>围绕本文设计的区块链需要解决额外的问题，最值得注意的是验证计算输出的机制。一方面，拆分共识和计算工作提高了 Flow 的吞吐量。另一方面，必须关注计算结果状态的正确性，因为共识节点不会重复计算。 此外，在 Flow 中，区块不再包含状态的hash结果。因此，接收数据的节点无法基于已发布区块验证数据有效性。我们将在后续论文中介绍区块验证和保证计算结果的（称为块密封）技术细节。</p><h2 id="h-5" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5 结论</h2><p>在这个理论证明中，我们证明了共识和计算的分离可以使得PoS区块链网络中的资源利用率显著提高。对于常规PoS网络，一个区块在下一个区块被提议之前完成，吞吐量受到一小部分最慢节点的限制。而共识和计算的分离显著地将计算负载从共识节点转移到最快的节点。定理1中表明，共识和计算分离不会牺牲网络的安全性。在简化的模型中，我们的模拟显示与共识节点包含共识和计算的架构相比，吞吐量增加了56倍。 作为以太坊，一种增加吞吐量的方式是设置算力要求。然而，这将使得新节点更加难以加入系统。在传统的工作量证明区块链中，大量计算资源集中在一小部分挖矿节点，而维护和更新状态在所有全节点上的计算量是一致的。 Flow 架构利用网络中自然的资源不平衡生态系统。少数具有海量计算和带宽能力的数据中心可以质押成为执行节点，以最有效地贡献其资源。相比之下，共识节点不存储或维护状态，因此可以在现有的个人设备上运行。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/77b211b7f2a39569fd9b3987cca064a2b5641b5a446e44d28e320471d50c7e26.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[以太坊技术系列-以太坊数据结构]]></title>
            <link>https://paragraph.com/@explorer-of-web3/euMkR58Oj3lLBAlViHhc</link>
            <guid>euMkR58Oj3lLBAlViHhc</guid>
            <pubDate>Mon, 24 Jan 2022 16:40:53 GMT</pubDate>
            <description><![CDATA[本篇文章和大家介绍一下以太坊的数据结构，上篇文章我们提到，以太坊为了实现智能合约这一功能，使用了基于账户的模型。我们来看看以太坊中数据结构。 账户状态树 哈希表 既然是基于账户的模型，我们需要通过账户地址找到账户的状态。就像通过银行卡号可以找到你在银行中的各种信息一样。最简单的想法当然是一个简单的哈希表 key是账户地址 value是账户状态。但这里有个问题解决不了。 轻节点如何校验账户合法性？ 上篇文章我们说过，区块链中有2类节点，全节点和轻节点，轻节点只会存储block header，所以轻节点如何才能校验账号是否合法呢？ Merkle Tree（哈希树） 这个思路和我们平时用的md5校验一致，我们会对区块内的信息进行hash运算从而得出区块内信息唯一确定的值，区块链所有节点中这个值都是相同的。 在这个过程中我们用到了一种数据结构Merkle Tree（哈希树），我们先看下Merkle Tree（哈希树）的示意图。上篇文章说到区块链中的链表(哈希链)和我们平时常见链表不同的是将指针从地址改为了hash指，这里也一样，哈希树和二叉树的区别有2个 1.将地址改为了哈希值 2.只有...]]></description>
            <content:encoded><![CDATA[<p>本篇文章和大家介绍一下以太坊的数据结构，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483660&amp;idx=1&amp;sn=d87ad60e8d3b68b03df3ac4cbbe5d875&amp;chksm=c1c99706f6be1e10810805f6492b3b55acf29b138cd5b653347ed4e7937282797fabfb0e35a5&amp;scene=21#wechat_redirect">上篇文章</a>我们提到，以太坊为了实现智能合约这一功能，使用了基于账户的模型。我们来看看以太坊中数据结构。</p><p><strong>账户状态树</strong></p><p><strong><em>哈希表</em></strong> 既然是基于账户的模型，我们需要通过账户地址找到账户的状态。就像通过银行卡号可以找到你在银行中的各种信息一样。最简单的想法当然是一个简单的哈希表 key是账户地址 value是账户状态。但这里有个问题解决不了。 轻节点如何校验账户合法性？</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://mp.weixin.qq.com/s?__biz=MzkyNTI4NzI2OQ==&amp;mid=2247483660&amp;idx=1&amp;sn=d87ad60e8d3b68b03df3ac4cbbe5d875&amp;chksm=c1c99706f6be1e10810805f6492b3b55acf29b138cd5b653347ed4e7937282797fabfb0e35a5&amp;scene=21#wechat_redirect">上篇文章</a>我们说过，区块链中有2类节点，全节点和轻节点，轻节点只会存储block header，所以轻节点如何才能校验账号是否合法呢？</p><p><strong><em>Merkle Tree（哈希树）</em></strong></p><p>这个思路和我们平时用的md5校验一致，我们会对区块内的信息进行hash运算从而得出区块内信息唯一确定的值，区块链所有节点中这个值都是相同的。</p><p>在这个过程中我们用到了一种数据结构Merkle Tree（哈希树），我们先看下Merkle Tree（哈希树）的示意图。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/bc618c057ab161641039d77ca0c1abe99a081c976b3886e5e69188737184ff68.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>上篇文章说到区块链中的链表(哈希链)和我们平时常见链表不同的是将指针从地址改为了hash指，这里也一样，哈希树和二叉树的区别有2个 1.将地址改为了哈希值</p><p>2.只有叶子节点存储数据</p><p>回到之前的问题轻节点是如何校验1个账户或交易是否是在链上的呢？</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5352490b75cbf8d8c5dfd729a68bdbe4fa347f702408ccbea85b7934556a3a8a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>整个流程如上图所示</p><p>1.轻节点需要判断1个账号是否合法</p><p>2.轻节点由于只存储block header，所以拿到1个账号的时候会向全节点发出请求 3.全节点存储了所有账户状态，将账户路径中的需要计算用到的hash值返回给轻节点</p><p>4.轻节点本地进行计算根hash值，如果计算结果和自己存储一致则账户合法，不一致则不合法。</p><p>那以太坊中的账户信息的数据结构就是这样吗？ 直接用这样的数据结构来存储账户信息会有2个问题</p><p>1.查找困难 2.生成hash值不确定</p><p>第1个问题应该比较容易发现，在这个树中寻找1个账号需要的复杂度是O(n),因为没有任何顺序。</p><p>第二个问题其实也是因为无序导致的，无序的组合每个节点针对同一批账户生成的hash值不一致，这就导致无法达成共识。</p><p><strong><em>字典树</em></strong> 既然2个问题都和顺序有关，那我们类似二叉排序树一样，使用哈希排序树是不是就可以解决问题了呢？ 使用排序树后会带来另外1个问题 插入困难 因为要维持树是有序的，很可能带来树结构的很大变动。</p><p>以太坊中使用了另外一种数据结构字典树。和哈希树不同，字典树应该是很多地方都有使用。我们简单来看下字典树的结构。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d5f446829c3d4e8c354a65de6bd7920c654b34d723a281cb3eb357a9e494f5ee.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>字典树能够较好地解决哈希树的2个缺点1.查找困难 2.生成的hash值不确定以及排序二叉树的1个缺点 插入困难。</p><p>但字典树我们可以看到可能树的深度可能由于部分元素导致整棵树深度非常深。</p><p><strong><em>压缩字典树</em></strong></p><p>这时我们可以进一步优化，将相同路径进行压缩。这就是压缩字典树。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/abee96f3bf393856452ff98e7edf38e7912b91fb65b0276594d56fdd160122ae.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong><em>MPT(哈希压缩字典树, 有专业翻译，但个人认为这样翻译比较易理解)</em></strong> 将哈希树和压缩字典树结合，就可以得到以太坊存储账户的最终数据结构-MPT。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e7043cd77194d933aeb8caeec8ffb2e24a2aa9014c6af1f046d90997651076ee.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>将压缩字典树里面的指针从地址改为指针，并且将数据存储在叶子节点中即可。</p><p><strong><em>状态树存储域</em></strong> 介绍完状态树的数据结构，我们接下来讨论1个问题，区块中存储的账户状态是什么样的范围。有2种选择。</p><ol><li><p>只保存当时区块中产生交易的账户状态。</p></li><li><p>保存全局所有的账户。</p></li></ol><p>我们可以看下这2种方式，无非就是空间和时间的平衡，只保存当前区块产生的交易意味着是做懒加载(需要的时候才去寻找账户)，在区块链中这个代价是非常大的，因为寻找的账户之前从未交易过，这样会遍历整个区块链。另外一种保存全局的账户方式虽然看起来空间消耗较大，但查找快捷，而且空间的问题我们可以通过其他方式优化。所以最终以太坊选择了第2种每个区块都报错全局所有账户的方式。</p><p>我们来看下以太坊中是如何保存状态树的。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/db7420a55df096c2eed938d0edbf2b408e1bf67de9250ac1336ea396c812b7fd.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>可以看到以太坊中虽然每个区块都保存了全部账户，但是会将未发生变化的账户状态指向前1个节点，本身只存储发生变化的状态，这样可以较大程度优化空间占用。</p><p><strong>交易树&amp;收据树</strong></p><p>介绍完以太坊中比较复杂的状态树后，我们继续来看看以太坊中的另外两棵树，交易树和收据树。 首先介绍一下，为什么需要交易树&amp;收据树。 1.交易树</p><p>虽然以太坊是基于账户的模型，但是就像银行不仅会存储银行卡的余额，还会存储卡中的每笔钱怎么来的以及怎么花的。交易树中就存储着当前区块中的包含的所有交易。</p><p>2.收据树 由于智能合约的引入增加了不少复杂性，所以以太坊用收据树存储着一些交易操作的额外信息。比如交易过程中执行日志就包含在收据树中方便查询。收据树和交易树是一一对应的。每发生一次交易就会有一次收据。</p><p>和状态树不同交易树和收据树只维护当前区块内发生的交易，因为当时区块发生交易时不需要再去查找另外1个交易，也就之前需要可能遍历整个区块链的查找操作了。</p><p>由于以太坊中的出块速度较快，我们进行一些查询一些符合条件交易的时候会面临大量数据遍历困难的问题。收据树中引入了布隆过滤器可以帮助我们有效缓解这一困难。</p><p><strong><em>布隆过滤器</em></strong> 布隆过滤器将大集合中每个元素进行hash运算映射到1个较小的集合，这时再来1个元素要判断是否在大集合的时候，不需要遍历整个大集合，而是去进行hash运算去小集合中寻找是否存在，如果不存在，肯定不在大集合中，如果存在则不能说明任何问题。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4a229a393c8d28d4a95a43659cf692fceaafd8e6873ad03c5295fe22b8e78135.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>如上图所示，布隆过滤器只能证明某1个元素不在集合中，不能证明1个元素在结合中。</p><p>以太坊中如果我们要在较多区块中寻找某1个交易，则可以利用布隆过滤器，过滤掉肯定不存在目标交易的区块，然后进入收据树内继续利用布隆过滤器筛选，剩下的才是可能的目标交易的交易，进行一一比对即可。</p><p><strong>总结</strong> 我们介绍了以太坊的核心数据结构，状态树&amp;交易树&amp;收据树，他们都是使用相同的数据结构-哈希压缩字典树。但状态树是维护1颗全局账户树，交易树和收据树则是维护本区块内的交易或收据。</p><p>介绍完数据结构后，后面我们会用几篇文章来介绍以太坊中的一些核心算法，比如共识机制，挖矿算法等。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
        </item>
        <item>
            <title><![CDATA[以太坊技术系列-以太坊中的账户]]></title>
            <link>https://paragraph.com/@explorer-of-web3/IsJDRWODIfCmZgBurf0K</link>
            <guid>IsJDRWODIfCmZgBurf0K</guid>
            <pubDate>Sat, 22 Jan 2022 09:21:06 GMT</pubDate>
            <description><![CDATA[区块链基础模型1.区块 图中的每个区块分为 block header 和 block body 两部分 包含着账户信息以及交易信息 具体信息如何存储后面再介绍以太坊中的数据结构h会具体介绍。 2.区块链 区间指针通过链式结构结合而成的就是我们经常说的区块链，只不过这个链表指针中存储的并非我们通常说的地址，而是存储了上一个区块的hash值。 3.全节点和轻节点 区块链是去中心化的。如何体现去中心，整个以太坊网络包含着多个很多个节点。 全节点存储着完整的区块链，每次数据的变更，需要通过共识机制，广播给所有全节点。轻节点存储着区块链中的block header部分(也是区块组成的链，只不过每个区块只包含block header)。 轻节点的作用只是用来查询，不对区块链中数据合法性负责，可以把我们自己的钱包理解为1个轻节点(这种理解在目前的市面上的钱包并不对，但可以先这样理解)。 基于账户模型的以太坊 这个是大部分区块链的通用模型。并没有体现出何为基于账户的模型。那是否还有其他模型？ 重点是刚刚有说，我们的区块block body中会存储账户信息。另外一种更著名的区块链应用-比特币并没有...]]></description>
            <content:encoded><![CDATA[<p>区块链基础模型</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a5b5abea164a0c219461dad202d9f24ad81d23426d37d35c081b7cb3f593eed2.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>1.区块</p><p>图中的每个区块分为 block header 和 block body 两部分</p><p>包含着账户信息以及交易信息  具体信息如何存储后面再介绍以太坊中的数据结构h会具体介绍。</p><p>2.区块链</p><p>区间指针通过链式结构结合而成的就是我们经常说的区块链，只不过这个链表指针中存储的并非我们通常说的地址，而是存储了上一个区块的hash值。</p><p>3.全节点和轻节点</p><p>区块链是去中心化的。如何体现去中心，整个以太坊网络包含着多个很多个节点。</p><p>全节点存储着完整的区块链，每次数据的变更，需要通过共识机制，广播给所有全节点。轻节点存储着区块链中的block header部分(也是区块组成的链，只不过每个区块只包含block header)。</p><p>轻节点的作用只是用来查询，不对区块链中数据合法性负责，可以把我们自己的钱包理解为1个轻节点(这种理解在目前的市面上的钱包并不对，但可以先这样理解)。</p><p>基于账户模型的以太坊</p><p>这个是大部分区块链的通用模型。并没有体现出何为基于账户的模型。那是否还有其他模型？</p><p>重点是刚刚有说，我们的区块block body中会存储账户信息。另外一种更著名的区块链应用-比特币并没有采用这种“基于账户的模型”, 而是采用基于交易的模型。</p><p><strong>基于交易的模型</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6368aba970beddd10550067bc7cbf7efa5b643d9f9bf58559f364e9850294387.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>如上图所示，比特币并不存储账户余额，而是存储每一笔交易。</p><p>我们再看下比特币是如何转账的。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2538ec8acb1426219c4ac2438c4f94946e70dbd4f4fe97ddbd0a6a0ec14940ce.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>当转账的时候，需要解释钱从何而来。</p><p>比特币在转账的时候会要求一次性全部转出，即需要把交易后的余额转入自己的另外1个账户(很多人的比特币因此丢失)。</p><p>比特币和以太坊2种不同的设计思路取决于其设计目的: 比特币是纯金融货币属性。所以更注重安全性，每次转账都进行账户切换。</p><p>以太坊为了要支持智能合约，合约是1个持续的过程，所以需要稳定的账户。</p><p><strong>应对攻击</strong></p><p>我们可以看看2种设计在面对一些常见攻击时是如何应对的。</p><p>1.双花攻击</p><p>双花攻击即一份钱花2次。为什么会出现双花攻击呢？主要原因是数字货币和纸币不同，复制是无成本，不可校验的。</p><p>比特币如何解决双花攻击，就是需要每次在花钱的时候说明钱从何而来，这样相当于用交易链去溯源。</p><p>以太坊面对双花攻击是自然屏蔽的，因为有了账户的概念，每花一次钱都需要将账户余额减小对应值。</p><p>2.重放攻击</p><p>重放攻击即将同一次交易多次发布。</p><p>比特币中由于使用了基于交易的模型，每次转账完成，钱都会到1个新的账户，所以重放攻击对比特币是无效的。</p><p>以太坊由于要维护账户的稳定，所以重放对以太坊是有威胁的，以太坊为了解决这个问题，引入了交易次数，每次交易的时候都会将交易次数一并签名，这样重放攻击者无法修改交易次数攻击无效。</p><p>可以看到由于最开始设计理念的不同，在遇到不同的攻击方式时，需要采用不同的策略去应对。</p><p>以太坊账户分类</p><p>以太坊中包含2种账户</p><p>1.外部账户，可以把钱包理解为1个外部账户，里面放着我们每个用户的余额。</p><p>2.合约账户，因为以太坊支持了智能合约，所以合约账户里面不仅包含着余额 还包含着代码和需要存储的数据。</p><p>我们在调用智能合约的过程，其实就是在和合约账户交互的过程。</p><p>常见的钱包</p><p>钱包的分类主要分为3类</p><p>1.交易所钱包  各大交易所的钱包，这种钱包其实并不能够受区块链保护，其掌控权是在交易所手中，但交易很比较方便。</p><p>2.热钱包，独立的钱包APP。</p><p>3.硬钱包，硬件钱包，无网络连接。</p><p>钱包中的常见术语</p><p>私钥(助记词）助记词是私钥的另外一种表现形式，类似ip到网址的转换更方便记忆。</p><p>公钥</p><p>私钥和公钥使用非对称加密算法来保护大家的钱包。</p><p>钱包密码：钱包密码是你为钱包设置的密码，但是钱包的安全性保障来自于私钥不被泄露而不是密码！</p><p>钱包地址：钱包地址是由公钥hash计算得到</p><p>个人使用较多的热钱包APP是 metamask: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://metamask.io/">https://metamask.io/</a></p><p>结尾</p><p>后续会继续用几篇文章来介绍以太坊技术，原因在于以太坊作为web3的基础所在，有必要学习内部原理，帮忙我们去更好地去深入讨论思考web3的后续发展。</p>]]></content:encoded>
            <author>explorer-of-web3@newsletter.paragraph.com (explorer_of_web3)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/a2a21628db96768e06df278c85f1259095210c51dbe524e8641f08c8afe593da.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>