智能合约

概念

智能合约只是一个运行在以太坊链上的一个程序。 它是位于以太坊区块链上一个特定地址的一系列代码(函数)和数据(状态)。

智能合约也是一个以太坊帐户,我们称之为合约帐户。 这意味着它们有余额,可以成为交易的对象。 但是,他们无法被人操控,他们是被部署在网络上作为程序运行着。 个人用户可以通过提交交易执行智能合约的某一个函数来与智能合约进行交互。 智能合约能像常规合约一样定义规则,并通过代码自动强制执行。 默认情况下,你无法删除智能合约,与它们的交互是不可逆的。

一般用 solidity 语言编写,官网open in new window

特征

无需准入性

任何人都可以编写智能合约并将其部署到区块链网络上。 只要有足够的以太币来部署合约即可。 部署智能合约在技术上是一笔交易,因此需要为简单的以太币转账支付燃料费一样,也需要为部署智能合约支付燃料费。 但是,合约部署的燃料成本要高得多。

可组合性

智能合约在以太坊上公开,并且可以看成开放应用程序接口。 这意味着可以在自己的智能合约中调用其他智能合约,以大幅扩展可能的功能。 合约甚至可以部署其他合约。

局限性

智能合约本身无法获取有关“现实世界”事件的信息,因为它们无法从链下来源检索数据。 这意味着它们无法对现实世界中的事件作出响应。 这是设计使然。 因为依赖外部信息可能会影响共识,而共识对安全性和去中心化而言十分重要。然而,对于区块链应用程序来说,能够使用链下数据非常重要。

智能合约的另一个限制是最大合约大小。 智能合约最大可达 24 KB,否则会消耗完燃料。 可以使用钻石模式open in new window来规避它。

多重签名合约

多重签名合约是需要多个有效签名才能执行交易的智能合约帐户。 这对于避免持有大量以太币或其他代币的合约出现单点故障非常有用。 多重签名还可以在多方之间划分合同执行和密钥管理的责任,并防止丢失单个私钥导致不可逆转的资金损失。 由于这些原因,多重签名合约可用于简单的去中心化自治组织治理。 多重签名需要 M 个可能的可接受签名中的 N 个签名才能执行(其中 N ≤ M,并且 M > 1)。 普遍使用 N = 3, M = 5N = 4, M = 7。 4/7 多重签名需要七个可能的有效签名中的四个。 这意味着即使失去了三个签名,资金仍然可以收回。 在这种情况下,这也意味着必须得到大多数密钥持有人的同意和签名才能执行合约。

消息调用

合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。 消息调用和交易非常类似,它们都有一个源,目标,数据,以太币,gas和返回数据。 事实上每个交易都由一个顶层消息调用组成,这个消息调用又可创建更多的消息调用。

合约可以决定它剩余的 gas 有多少应该随内部消息调用一起发送,有多少它想保留。 如果在内部调用中发生了out-of-gas的异常(或任何其他异常),这将由一个被压入栈顶的错误值来表示。 在这种情况下,只有与调用一起发送的gas被用完。 在Solidity中,在这种情况下,发起调用的合约默认会引起一个手动异常, 所以异常会在调用栈上 “冒泡出来”。

被调用的合约(可以与调用者是同一个合约)将收到一个新清空的内存实例, 并可以访问调用的有效负载-由被称为 calldata 的独立区域所提供的数据。 在它执行完毕后,它可以返回数据,这些数据将被存储在调用者内存中由调用者预先分配的位置。 所有这样的调用都是完全同步的。

调用被 限制 在1024的深度,这意味着对于更复杂的操作,循环应优先于递归调用。 此外,在一个消息调用中,只有63/64的gas可以被转发,这导致在实践中,深度限制略低于1000。

委托调用和库

存在一种特殊的消息调用,被称为 委托调用(delegatecall), 除了目标地址的代码是在调用合约的上下文(即地址)中执行, msg.sendermsg.value 的值不会更改之外,其他与消息调用相同。

这意味着合约可以在运行时动态地从不同的地址加载代码。 存储,当前地址和余额仍然指的是调用合约,只是代码取自被调用的地址。

这使得在Solidity中实现 “库” 的功能成为可能: 可重复使用的库代码,可以放在一个合约的存储上,例如,用来实现复杂的数据结构的库。

智能合约结构

智能合约是一种在以太坊某个地址上运行的程序。 它们是由数据和函数组成的,可以在收到交易时执行。 以下概述一个智能合约的组成。

数据

任何合约数据必须分配到一个位置:要么是存储,要么是内存。 在智能合约中修改存储消耗很大,因此你需要考虑数据在哪里存取。

存储

持久性数据被称之为存储,由状态变量表示。 这些值被永久地存储在区块链上。 你需要声明一个类型,以便于合约在编译时可以跟踪它在区块链上需要多少存储。

这些应该就是保存在 stateDB 中的数据,是一个合约的“全局变量”,也包括合约本身数据

数据类型如下:

  • 布尔
  • 整数(integer)
  • 定点数(fixed point numbers)
  • 固定大小的字节数组(fixed-size byte arrays)
  • 动态大小的字节数组(dynamically-sized byte arrays)
  • 有理数和整数常量(Rational and integer literals)
  • 字符常量(String literals)
  • 十六进制常量(Hexadecimal literals)
  • 枚举(Enums)
  • address 类型可以容纳一个以太坊地址,相当于 20 个字节或 160 位。 它以十六进制的形式返回,前导是 0x。

内存

仅在合约函数执行期间存储的值被称为内存变量。 由于这些变量不是永久地存储在区块链上,所以它们的使用成本要低得多。可以认为是运行时数据(局部变量)。也就是程序运行栈。

环境变量

除了在自己合约上定义的变量之外,还有一些特殊的全局变量。 它们主要用于提供有关区块链或当前交易的信息。

示例:

属性状态变量描述
block.timestampuint256当前区块的时间戳
msg.sender地址消息的发送者(当前调用)

函数

用最简单的术语来说,函数可以获得信息或设置信息,以响应传入的交易。

有两种函数调用方式:

  • internal:不会创建以太坊虚拟机调用
    • Internal 函数和状态变量只能在内部访问(只能在合约内部或者从其继承的合约内部访问)。
  • external:会创建以太坊虚拟机调用
    • External 函数是合约接口的一部分,这意味着他可以被其它合约和交易调用。 一个 external 函数 f 不可以被内部调用(即 f() 不行,但 this.f() 可以)。

它们可以是 publicprivate

  • public 函数可以在合约内部调用或者通过消息在合约外部调用
  • private 函数仅在其被定义的合约内部可见,并且在该合约的派生合约中不可见。

函数和状态变量都可以被定义为 public 或 private

下面是更新合约上一个状态变量的函数:

// Solidity example
function update_name(string value) public {
    dapp_name = value;
}
  • string 类型的参数 value 传入函数 update_name
  • 函数声明为 public,意味着任何人都能访问它
  • 函数没有被声明为 view,因此它可以修改合约状态

视图函数 view 函数

这些函数保证不会修改合约数据的状态(就可以理解为只是读取合约数据)。 常见的示例是 "getter" 函数 - 例如,它可以用于接收用户的余额。

与之对应的还有 payable 和 pure 函数等等。。。。当一个函数被 payable 修饰,表示调用这个函数时,可以附加发送一些 ETH。pure 修饰的函数 ,不能对storage变量进行读写。view 修饰的函数,只能读取storage变量的值,不能写入。

function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerPizzaCount[_owner];
}

这些操作被视为修改状态:

  1. 写入状态变量。
  2. 正在导出事件。
  3. 创建其它合约。
  4. 使用 selfdestruct
  5. 通过调用发送 ether。
  6. 调用任何未标记为 viewpure 的函数。
  7. 使用底层调用。
  8. 使用包含某些操作码的内联程序组。

构造函数

constructor 函数只在首次部署合约时执行一次。 与许多基于类的编程语言中的 constructor 函数类似,这些函数常将状态变量初始化到指定的值。

// 初始化合约数据,设置 `owner`为合约的创建者。
constructor() public {
    // 所有智能合约依赖外部交易来触发其函数。
    // `msg` 是一个全局变量,包含了给定交易的相关数据,
    // 例如发送者的地址和交易中包含的 ETH 数量。
    owner = msg.sender;
}

内置函数

除了自己在合约中定义的变量和函数外,还有一些特殊的内置函数。 最明显的例子是:

  • address.send() – Solidity

这使合约可以发送以太币给其它帐户。

事件和日志

事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中。

来捋这个关系:区块链是打包一系列交易的区块组成的链条,每一个交易“收据”会包含0到多个日志记录,日志代表着智能合约所触发的事件。

在DAPP的应用中,如果监听了某事件,当事件发生时,会进行回调。 不过要注意:日志和事件在合约内是无法被访问的,即使是创建日志的合约。

事件可以让你通过前端或其它订阅应用与你的智能合约通信。 当交易被挖矿执行时,智能合约可以触发事件并且将日志写入区块链,然后前端可以进行处理。

**如果函数会触发事件,则不能将其定义为viewpure。**这是因为触发事件会将数据写入区块链(到日志中)。

事件的参数可以使用 indexed 关键字(一个事件最多只能 3 个),从而构成 topics,变成可检索的日志。

比如:

// 事件允许在区块链上记录活动。
// 以太坊客户端可以监听事件,以便对合约状态更改作出反应。
// 了解更多: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events
event Transfer(address from, address to, uint amount);

可以在函数内触发事件,记录日志

// 从任何调用者那里发送一定数量的代币到一个地址。
function transfer(address receiver, uint amount) public {
    // 发送者必须有足够数量的代币用于发送
    require(amount <= balances[msg.sender], "Insufficient balance.");
    // 调整两个帐户的余额
    balances[msg.sender] -= amount;
    balances[receiver] += amount;
    // 触发之前定义的事件。
    emit Transfer(msg.sender, receiver, amount);
}

有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。 这个特性被称为 日志(logs) ,Solidity用它来实现 事件open in new window。 合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。 因为部分日志数据被存储在 布隆过滤器(bloom filter)open in new window 中, 我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

bin&abi

solc --abi --bin -o ./dir xxx.sol

在以太坊智能合约的开发过程中,编译智能合约会生成两种重要的文件:bin 文件和 abi 文件。每个文件都有特定的用途和内容。

bin 文件

bin 文件包含智能合约的字节码(Bytecode)。字节码是智能合约在以太坊虚拟机(EVM)上执行的==机器级代码==。当智能合约被部署到以太坊网络上时,部署的实际上是这个字节码。

  • 生成方式: 使用 Solidity 编译器(如 solc)编译智能合约源码时生成。
  • 用途: 部署智能合约到以太坊网络,并在网络上运行合约代码。

abi 文件

abi 文件包含应用二进制接口(Application Binary Interface)。ABI 描述了智能合约的接口,包括其可调用的函数及其参数、返回值类型、事件等。ABI 是以 JSON 格式表示的。

  • 生成方式: 使用 Solidity 编译器(如 solc)编译智能合约源码时生成。
  • 用途: 允许前端应用程序与智能合约进行交互,了解如何调用合约的函数和解析返回值。

示例

// SimpleStorage.sol
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

编译后的 bin 文件(简化版)

608060405234801561001057600080fd5b506101...

(这是一段很长的字节码)

编译后的 abi 文件

[
    {
        "constant": false,
        "inputs": [
            {
                "name": "x",
                "type": "uint256"
            }
        ],
        "name": "set",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "get",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    }
]

总结

  • bin 文件: 包含智能合约的字节码,用于部署合约。
  • abi 文件: 包含智能合约的接口描述,用于与合约进行交互。

这两个文件在智能合约的开发和使用过程中都至关重要。字节码用于将合约部署到区块链上,而 ABI 则用于与已经部署的合约进行交互。

Last Updated:
Contributors: liushun-ing