• solidity开篇:区块链基础


    solidity开篇:区块链基础

    1、事务

    2.交易

    3.地址

    4.区块

    5.存储/内存/栈

    2️⃣Hello World

    1.例子代码

    2.Hello World 例子分析

    3️⃣ 合约代码中的三种注释

    1.单行注释

    2.块注释

    3.NatSpec 描述注释


    solidity开篇:区块链基础

    Solidity 是在兼容 EVM 的区块链上开发智能合约的语言,我们不需要关心所在区块链底层逻辑,只要是兼容 EVM 的公链,我们都可以使用 Solidity 进行智能合约的编码。简单了解以下的区块链概念:

    • 事务

    • 交易

    • 地址

    • 区块

    • 存储/内存/栈

    1、事务

    事务意味着你想做的事情,要么一点没做,要么全部完成。具有原子性,不存在修改一半的情况。

    比如从 A 地址向 B 地址转账 100 元,那么数据库里 A 减 100 元,B 加 100 元。如果因为某些原因导致 A 已经减了 100 元,但是 B 加 100 元中间出现了异常。因为事务的原子性,发生失败后 A/B 地址都不会发生任何修改。这种场景在合约中经常发生,会经常看到 out of gas 异常,这是因为 gas 被耗尽。此时合约中做的所有修改都会被回滚。

    gas:合约的手续费;是作为用户为当前交易支付的手续费,每一笔交易都会收取 gas 费,目的是限制交易需要做的工作量,需要做的事情越多,所花费的 gas 也就越多;gas 会按照特定规则进行逐渐消耗,如果执行完成后还有剩余,gas 会在当前交易内原路返回到交易发起者的地址中。

    2.交易

    交易可以看作一个地址发送到另外一个地址的消息,可能包含一个二进制数据和以太币。

    • 如果目标地址含有代码,则此代码会被执行,并以 payload 作为入参。

    • 如果目标地址是零地址,此交易将创建一个新合约。

      • 这时候用来创建合约的 payload 会被转为 EVM 字节码执行,执行的输出作为合约代码永久存在区块链上。

      • 所以如果创建一个合约,并不需要向链上发送实际的合约代码,只需发送能够产生合约代码的代码就可以。

    区块链中的交易遵守事务的特性。交易总是由发送人(创建交易的地址)进行签名。区块链底层会确保只有持有该地址密钥才能发起交易。正因为这个特性,所以才能为区块链上特定状态的修改增加保护机制。

    比如在合约中指定某一个方法只有"管理员"账号可以用,我们只需要验证调用者是否为管理员地址就可以了,至于地址权限的保护事情并不需要关心,只要是该账号发起的交易,就认为是管理员在操作。安全方面我们需要考虑的是,如果某一个地址被盗了怎么样,通常这些是业务逻辑决定,比如多签钱包的业务。

    3.地址

    地址很多时候也被称为账户,EVM 中有两类地址,一类是外部地址,一类是合约地址

    • 外部地址:由公钥-私钥对控制

      • 常用的助记词,keystore 文件等只是方便用户储存,底层还是会转成私钥。

      • 一般是钱包应用创建的地址。公钥就是0xABC的这种以太坊收款地址,私钥可能是助记词生成,可能是 keystore 文件生成,也可能是用户直接保存的。

    • 合约地址:由地址一起存储的代码控制。

    无论外部地址,还是合约地址,对于 EVM 来说,都是一样的。每个地址都有一个键值对形式的持久化存储。其中 key 和 value 都是 256 位,我们称为存储。此外每个地址都会有一个以太币的余额,合约地址也是如此;余额会因为发送包含以太币的交易而改变。

    4.区块

    你可能听过区块链的双花攻击,女巫攻击等作恶方式。如果你没有听过也没有关系,因为它们对于智能合约开发来说并不重要,我们编写的 Solidity 代码能运行在以太坊网络,也可以运行在 BSC, Matic,Eos EVM 网络等,就像前文说的那样,无论他们采用什么底层逻辑,只要它们支持 EVM 就足够了,底层逻辑不用关心。

    我们需要关心的是,区块可能被回滚,交易可能被作废,所以会出现你发起的交易被回滚甚至从区块链中抹除掉的可能。区块链不能保证当前的交易一定包含在下一个区块中。如果你开发的合约有顺序关系,要注意这个特性。合约内的逻辑,不能将某一个块作为依赖。

    5.存储/内存/栈

    存储:每一个地址都有一个持久化的内存,存储是将 256 位字映射到 256 位字的键值存储区。所以数据类型的最大值是 uint256/int256/bytes32,合约只能读写存储区内属于自己的部分。

    内存:合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。所以储存在内存中的数据,在函数执行完以后就会被销毁。内存是线性的,可按字节级寻址,但读的长度被限制为 256 位,而写的长度可以是 8 位或 256 位。

    :合约的所有计算都在一个被称为栈(stack)的区域执行,栈最大有 1024 个元素,每一个元素长度是 256 bit;所以调用深度被限制为 1024 ,对复杂的操作,推荐使用循环而不是递归。

    2️⃣Hello World

    Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例中函数时,会切换执行时的上下文,此时前一个合约的状态变量就不能访问了。后面会逐步展开介绍,国际惯例,使用当前语言的 Hello World 作为第一个例子。

    1.例子代码

    1. // SPDX-License-Identifier: MIT
    2. pragma solidity ^0.8.17;
    3. contract Hello {
    4.    // 24509 gas
    5.    string public message = "Hello World!"; // 状态变量
    6.    // 24473
    7.    function fn1() public view returns (string memory) {
    8.        return message;
    9.   }
    10.    // 21801. 内存中直接返回
    11.    function fn2() public pure returns(string memory){
    12.        return "Hello World!";
    13.   }
    14.    // 21880
    15.    function fn3() public pure returns(string memory){
    16.        return fn2(); // 使用方法;函数调用函数,没有this。直接调用
    17.   }
    18. }

    2.Hello World 例子分析

    上面的代码获取 message 可以得到 "Hello World!",调用 fn1()函数,也可以得到 "Hello World!"; 这是因为 fn1 里面的逻辑是返回 message。通过这个例子可以发现,合约内调用变量并不需要使用 this 之类的关键字,直接使用即可,调用函数也是如此,直接 fnName([x]) 就可以。

    通过 Remix 调用详情我们可以发现,他们消耗的 gas 不相同。通常直接获取 message 更省钱,因为message储存在状态变量中,而函数helloWorld是读取了状态变量然后再返回出去。但是在 Remix 中有时候得到的结果却并不相同,不用太相信 Remix 内的 gas。在 Remix 中,代码顺序,变量名/函数名长短的修改都可以大大影响 gas 消耗,不要太相信 Remix 的 ga 消耗。

    在编写 solidity 代码时,保证安全的前提下,让合约消耗更少的 gas 是一个重要的优化方向。后面会有专门的进行 gas 优化的探讨,这里不再多展开。

    3️⃣ 合约代码中的三种注释

    我们看到第一行的代码是 // SPDX-License-Identifier: MIT 这里面的 // 符号,是注释符。用来标记和记录代码开发相关的事情,注释的内容是不会被程序运行,Solidity 支持单行注释和块注释,注释是为了更好的解释代码。请不要相信好的代码不需要注释这种鬼言论。代码中加入注释可以更好的团队协作,让自己更好的进行代码开发,以及让阅读者更快捷的理解代码逻辑。在实际工作中经常会出现自己写的代码一年半载之后再看,复杂些的逻辑可能需要浪费很多时间在代码理解上,如果再没有设计图和代码注释,简直想骂人。

    Solidity 支持 3 种注释方式;

    • 单行注释

    • 块注释

    • NatSpec 描述注释

    1.单行注释

    格式: // 注释内容

    // SPDX-License-Identifier: MIT
    string message = "Hello World!"; // 这是单行注释

    如上,// 后面的内容都会被编译器忽略,为了可读性,一般会在//后面加一个空格。

    2.块注释

    格式如下,在 /**/ 之间的内容,都被编译器忽略

        /*
        这是块注释
        */

    为了可读性,一般块注释的行首都加 * 和空格,如下

        /**
         * 这是块注释
         * 这是块注释
         */

    3.NatSpec 描述注释

    单行使用 /// 开始,多行使用 /** 开头以 */ 结尾。NatSpec 描述注释的作用非常重要,它是为函数、返回变量等提供丰富的文档。**在编写合约的时候,强烈推荐使用 NatSpec 为所有的开放接口(只要是在 ABI 里呈现的内容)进行完整的注释。**

    ⓵ 简单演示

    1. // SPDX-License-Identifier: MIT
    2. pragma solidity ^0.8.17;
    3. /// @title  一个简单的数据存储演示
    4. /// @author kyp
    5. /// @notice 您智能将此合约用于最基本的演示
    6. /// @dev    提供了存储方法/获取方法
    7. /// @custom:xx   自定义的描述/这个是实验的测试合约
    8. contract  TinyStorage {
    9.    // data
    10.    uint256 storedData;
    11.    /// @notice 储存 x
    12.    /// @param _x: storedData 将要修改的值
    13.    /// @dev   将数字存储在状态变量 storedData 中
    14.    function set(uint256 _x) public{
    15.        storedData = _x;
    16.   }
    17.    /// @notice 返回存储的值
    18.    /// @return 储存值
    19.    /// @dev   检索状态变量 storedData 的值
    20.    function get() public view returns(uint256){
    21.        return storedData;
    22.   }
    23.    /**
    24.     * @notice 第二种写法
    25.     * @param _x: XXXXX
    26.     * @dev   XXXXX
    27.     * @return XXXXX
    28.     * @inheritdoc :
    29.     */
    30. }

    上面所有标签都是可选的。下表解释了每个 NatSpec 标记的用途以及可以使用在哪些位置。我们可以选择合适的标记进行记录

    标签说明语境
    @title描述 contract/interface 的标题contract, interface, library
    @author作者姓名contract, interface, library
    @notice向最终用户解释这是做什么的contract, interface, library, function, 公共状态变量 event
    @dev向开发人员解释任何额外的细节contract, interface, library, function, 状态变量, event
    @param记录参数(后面必须跟参数名称)function, event, 自定义错误
    @return函数的返回变量function, 公共状态变量
    @inheritdoc从基本函数中复制所有缺失的标签(必须后跟合约名称)function, 公共状态变量
    @custom:...自定义标签,语义由应用程序定义所有位置均可以

    ⓶ 文档输出

    使用 NatSpec 描述注释的另一个好处是,当被编译器解析时,上面示例中的代码将生成两个不同的 JSON 文件。

    • User Documentation:供最终用户在执行功能时作为通知使用的

    • Developer Documentation:供开发人员使用的。

    如果将上述合约另存为,a.sol 则您可以使用以下命令生成文档:

    solc --userdoc --devdoc a.sol

    ⓷ 继承说明

    TODO: 在后面合约继承的时候再演示使用。

    如果函数是继承别的合约,没有 NatSpec 的函数将自动继承其基本函数的文档。但是下面三种情况是例外的:

    • 当参数名称不同时。

      • 这时候是函数的重载,函数签名已经发生了改变。

    • 当有多个基本功能时。

      • 这时候因为发生了冲突,supper 中有多个父级

    • 当有一个明确的 @inheritdoc 标签指定应该使用哪个合约来继承时。

    更多 NatSpec 请参考: GitHub - aragon/radspec: 🤘 Radspec is a safe interpreter for Ethereum's NatSpec

  • 相关阅读:
    设计模式之建造者模式
    Javascript EventListener 事件监听 (mouseover、mouseout)
    猿创征文 第二季| #「笔耕不辍」--生命不息,写作不止#
    滴滴 Redis 异地多活的演进历程
    lvgl overview
    图神经网络(六):GAT
    azkaban表project_flows数据分析
    中企绕道突破封锁,防不胜防 | 百能云芯
    金仓数据库 KingbaseGIS 使用手册(6.5. 几何对象编辑函数)
    unity学习(55)——选择角色界面--解析赋值服务器返回的信息2
  • 原文地址:https://blog.csdn.net/djklsajdklsajdlk/article/details/128035195