solidity基础学习

0 参考来源

文档主要参考了https://cryptozombies.io的游戏dapp开发网站,一步步通过游戏来讲解solidity。

同时一些细节参考了网上的博客:
https://www.colabug.com/2416565.html
https://www.cnblogs.com/StephenWu/p/7096550.html
https://blog.csdn.net/liyuechun520/article/details/78408588
等等

附一些solidity和以太坊的参考资料:
official docs: http://www.ethdocs.org/en/latest
以太坊wiki:https://github.com/ethereum/wiki/wiki

以及github上的solidity指南合集:https://github.com/bkrem/awesome-solidity

1 solidity支持的数据类型

1.1 关键词罗列

string//字符串
uint //默认为uint256,还有uint8,uint128等等
int//默认为int256
[] //数组
struct //结构体
view //修饰函数,函数只读取数据不更改数据
pure //修饰函数,函数不访问应用中的数据,返回值完全取决于它的输入参数
mapping//映射:存储和查找数据所用的键-值对
address//地址
bytesN//N取1到32的任意整数值,默认的byte表示bytes1
require//限制条件,require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行,个人感觉类似assert断言
inheritance//继承,合约可以访问被继承合约中定义的public函数
storage//永久存储在区块链中的变量
memory//临时变量,当外部函数对某合约调用完成时,内存型变量即被移除
//大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,//并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
//有时,也需要手动声明存储类型,主要用于处理函数内的 _ 结构体 _ 和 _ 数组 _ ,用以标识指针指向的是原始上链数据还是副本

internal//类似private,但是合约可以访问父合约中定义的internal函数
external//与public类似,但是这些函数只能在合约之外调用
constructor()//0.4.22及以后采用此作为构造函数
modifier//函数修饰符,修饰函数,它不能像函数那样被直接调用,只能被添加到函数定义的末尾,用以改变函数的行为,在其他语句执行前,先检查先验条件,给继承这个函数的modifier的对应函数加一个特定约束,在继承的函数体将在_特殊符号的位置被插入
now//当前unix时间戳(自1970年1月1日以来经过的秒数)

Solidity 定义的函数的属性默认为公共。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。

在 Solidity 中,有一些全局变量可以被所有函数调用, msg.sender,它指的是当前调用者(或智能合约)的 address

Solidity 并不支持原生的字符串比较, 我们只能通过比较两字符串的 keccak256 哈希值来进行判断

1.2 参数类型细节

1.2.1 数组

https://www.jianshu.com/p/5ca861cce245

1.2.2 memory storage

引用类型的变量在传值时需要memory或storage关键词,一般用于结构体struct、数组或者mapping当中,具体而言,就是传入的值是一个副本还是原始参数。
例如:

struct Zombie {
    uint a;
    bool b;
}
function modify(Zombie storage _zombie) internal {
    _zombie.a = 6;//这样就修改了传入对象的原始值
}
function fake_modify(Zombie memory _zombie) internal {
    _zombie.a = 5;//这样只是修改了临时变量的值
}

注:函数参数如果为storage类型时,函数的类型必须为internal或者private。

1.2.3 函数修饰符

可以带参数

// 存储用户年龄的映射
mapping (uint => uint) public age;

// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序逻辑
}

1.2.4 view

当玩家从外部调用一个view函数,是不需要支付一分 gas 的。这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。

注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。

1.2.5 数组

    uint[] a = new uint[](3);//注意在括号里写length
    int b;
    constructor() public {
        a.push(1);//可以通过push动态扩大长度
        a.push(2);
        a.push(3);
        a.pop();//弹出
        a.length;//数组长度
    }

    function seta(uint _a) internal pure {
        _a = 3;
    }
    
    function query() public view returns(uint) {
        return a[2];
    }

1.2.6 事件

事件 是合约和区块链通讯的一种机制。前端应用“监听”某些事件,并做出反应。
比如:

// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
  uint result = _x + _y;
  //触发事件,通知app
  IntegersAdded(_x, _y, result);
  return result;
}

app 前端可以监听这个事件。JavaScript 实现如下:

YourContract.IntegersAdded(function(error, result) { 
  // 干些事
}

1.2.7 modifier函数修饰符

modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}


contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  //注意! `onlyOwner`上场 :
  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

当你调用 likeABoss 时,首先执行 onlyOwner 中的代码, 执行到 onlyOwner 中的 _; 语句时,程序再返回并执行 likeABoss 中的代码。

思考:直接在函数中添加require(…)不是一样吗?意义在于什么?
简化一部分通用权限处理的编写数量?

1.2.8 时间单位

秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years)

1 days//也需要用复数

now 返回类型 uint256

2 一个小栗子

pragma solidity ^0.4.24;//定义版本

import "./ownable.sol";//可以调用其他合约

contract ZombieFactory is Ownable { //is 继承

    event NewZombie(uint zombieId, string name, uint dna);//事件

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Zombie {//结构体
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;//映射
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);//触发事件,高版本需要添加emit
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

3 开发环境搭建

3.1 在线环境(推荐)

remix:https://remix.ethereum.org

缺点:不稳定,时常打不开。

优点:功能强大,包含编译、调试、生成abi等功能,可以选择各种版本的编译器。


可以看到,当选择编译器版本过高时,构造函数的问题被解析出来。

3.2 离线环境

3.2.1 remix离线

代码仓库:https://github.com/ethereum/remix-ide/

安装:npm安装

npm install remix-ide -g
remix-ide

or git下载代码仓库安装

git clone https://github.com/ethereum/remix-ide.git
git clone https://github.com/ethereum/remix.git # only if you plan to link remix and remix-ide repositories and develop on it.
cd remix-ide
npm install
npm run setupremix  # only if you plan to link remix and remix-ide repositories and develop on it.
npm start

报错:

 fatal: unable to connect to github.com:
npm ERR! github.com[0: 13.229.188.59]: errno=No such file or directory
npm ERR! github.com[1: 13.250.177.223]: errno=No such file or directory
npm ERR! github.com[2: 52.74.223.119]: errno=No such file or directory

可以在本地跑一个remix:在http://127.0.0.1:8080开启本地remix窗口。

缺点:只包含了最新版本的编译器,不支持老版本。

3.2.2 vscode

安装Juan Blanco的solidity插件。

优点:本地编译。

缺点:无法调试。且如果需要更换低版本编译器,需要自己动手修改。

4 部署与调试

4.1 运行

remix可以通过javascript虚拟环境来模拟客户端连入一个虚构的区块链网络,从而进行合约测试。

会预先提供一些含有ether的账户供测试使用,足够合约运行。

和真实以太坊一样,部署运行合约需要消耗gas。

部署运行成功后,浏览器会自动生成可以外部调用的方法栏,输入参数即可观察合约方法运行结果。

4.2 调试

???

5 多源文件

solidity支持多源文件的编写。

多个源文件的合约部署:管理平台能够做到吗?

行内wifi,使用不了cita的js sdk!!!

6 与其他合约的交互

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。

假设在区块链上有这么一个合约:

contract LuckyNumber {
  mapping(address => uint) numbers;

  function setNum(uint _num) public {
    numbers[msg.sender] = _num;
  }

  function getNum(address _myAddress) public view returns (uint) {
    return numbers[_myAddress];
  }
}

假设我们有一个外部合约,使用 getNum 函数可读取其中的数据。

首先,我们定义 LuckyNumber 合约的 interface :

contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

我们可以在合约中这样使用:

contract MyContract {
  address NumberInterfaceAddress = 0xab38...;
  // ^ 这是FavoriteNumber合约在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  // 现在变量 `numberContract` 指向另一个合约对象

  function someFunction() public {
    // 现在我们可以调用在那个合约中声明的 `getNum`函数:
    uint num = numberContract.getNum(msg.sender);
    // ...在这儿使用 `num`变量做些什么
  }
}

7 外部常用库

7.1 OpenZeppelin库 Ownable合约(禁止第三方修改自己的合约,同时留有后门函数可以自己修改)

设置合约中函数的所有权(执行权)

大多数人开发自己的 Solidity DApps,都是从复制/粘贴 Ownable 开始的,从它再继承出的子类,并在之上进行功能开发。

附合约:

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }


  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }


  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

}

8 DApp

8.1

部署在以太坊上的 DApp,并不能保证它真正做到去中心,你需要阅读并理解它的源代码,才能防止其中没有被部署者恶意植入后门。作为开发人员,如何做到既要给自己留下修复 bug 的余地,又要尽量地放权给使用者,以便让他们放心你,从而愿意把数据放在你的 DApp 中,这确实需要个微妙的平衡。

8.2 与众不同的特征——Gas驱动

一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。

以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的“去中心化” 由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。

可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费。

8.3 如何节省Gas

通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何 gas。除非,把uint绑定到struct里面。

如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30); 

所以,当 uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct:

uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;

前者比后者需要的gas更少,因为前者把uint32放一起了。

9 开发注意事项

9.1 public external仔细排查

需要仔细地检查所有声明为 public 和 external的函数,一个个排除用户滥用它们的可能,谨防安全漏洞。请记住,如果这些函数没有类似 onlyOwner 这样的函数修饰符,用户能利用各种可能的参数去调用它们。

9.2 利用’view’函数节省gas

外部调用一个view函数,是不需要支付一分 gas 的。

9.3 storage慎用!

使用storage(存储)是相当昂贵的,”写入“操作尤其贵。

这是因为,无论是写入还是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长,拷贝份数更多,存储量也就越大。这是需要成本的!

9.4 反人类的做法

为了节省gas,当需要查询的时候,不建议使用mapping即类似哈希的方式,因为这样会多余写入。

反而需要使用遍历的方法,因为这样通过view虽然查询变慢,但是花费gas也较少。

其他概念

1 侧链

问题:

1 address(0)

来源:

ownable合约

function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0)); //这里的address(0)是什么意思?如果是任意新地址,那不是怎么都通不过了?
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

solidity中:是一个初始化的地址或者就是0x0?

以太坊交易中:

transaction = {
  nonce: '0x0', 
  gasLimit: '0x6acfc0', // 7000000
  gasPrice: '0x4a817c800', // 20000000000
  to: '0x0',
  value: '0x0',
  data: '0xfffff'
};

a new contract will be created by executing the code in data (this is what is meant by “code that returns the code”). The address of the newly created contract is technically known beforehand as it’s based on the address of the sender and it’s current nonce. That address becomes the official address of the contract after mining.

2 bytesN表示什么?

3 数组可以作为函数参数吗?

你可能感兴趣的:(以太坊)