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.例子代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
​
contract Hello {
    // 24509 gas
    string public message = "Hello World!"; // 状态变量
​
    // 24473
    function fn1() public view returns (string memory) {
        return message;
    }
​
    // 21801. 内存中直接返回
    function fn2() public pure returns(string memory){
        return "Hello World!";
    }
​
    // 21880
    function fn3() public pure returns(string memory){
        return fn2(); // 使用方法;函数调用函数,没有this。直接调用
    }
}

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 里呈现的内容)进行完整的注释。**

⓵ 简单演示

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
​
/// @title  一个简单的数据存储演示
/// @author kyp
/// @notice 您智能将此合约用于最基本的演示
/// @dev    提供了存储方法/获取方法
/// @custom:xx    自定义的描述/这个是实验的测试合约
contract  TinyStorage {
    // data
    uint256 storedData;
​
    /// @notice 储存 x
    /// @param _x: storedData 将要修改的值
    /// @dev   将数字存储在状态变量 storedData 中
    function set(uint256 _x) public{
        storedData = _x;
    }
​
    /// @notice 返回存储的值
    /// @return 储存值
    /// @dev   检索状态变量 storedData 的值
    function get() public view returns(uint256){
        return storedData;
    }
​
​
    /**
     * @notice 第二种写法
     * @param _x: XXXXX
     * @dev   XXXXX
     * @return XXXXX
     * @inheritdoc :
     */
}

上面所有标签都是可选的。下表解释了每个 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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值