引
智能合约数据来源于链上,智能合约的执行在链上,智能合约输出在链上。[1]
什么是智能合约
智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容
智能合约的账户保存了合约当前的运行状态
- balance 当前余额
- nonce 交易次数
- code 合约代码
- storage 存储,数据结构是一颗MPT
solidity
Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在以太坊虚拟机(EVM)上运行。Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。使用 Solidity 语言,可以为投票、众筹、秘密竞价(盲拍)、多重签名的钱包以及其他应用创建合约。[2]
简单的公开拍卖示例
每个人都可以在投标期内发送他们的出价。 出价已经包含了资金/以太币,来将投标人与他们的投标绑定。 如果最高出价提高了(被其他出价者的出价超过),之前出价最高的出价者可以拿回她的钱。 在投标期结束后,受益人需要手动调用合约来接收他的钱 - 合约不能自己激活接收。
pragma solidity ^0.4.22;
contract SimpleAuction {
// 拍卖的参数。
address public beneficiary;
// 时间是unix的绝对时间戳(自1970-01-01以来的秒数)
// 或以秒为单位的时间段。
uint public auctionEnd;
// 拍卖的当前状态
address public highestBidder;
uint public highestBid;
//可以取回的之前的出价
mapping(address => uint) pendingReturns;
// 拍卖结束后设为 true,将禁止所有的变更
bool ended;
// 变更触发的事件
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// 以下是所谓的 natspec 注释,可以通过三个斜杠来识别。
// 当用户被要求确认交易时将显示。
/// 以受益者地址 `_beneficiary` 的名义,
/// 创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。
constructor(
uint _biddingTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
auctionEnd = now + _biddingTime;
}
/// 对拍卖进行出价,具体的出价随交易一起发送。
/// 如果没有在拍卖中胜出,则返还出价。
function bid() public payable {
// 参数不是必要的。因为所有的信息已经包含在了交易中。
// 对于能接收以太币的函数,关键字 payable 是必须的。
// 如果拍卖已结束,撤销函数的调用。
require(
now <= auctionEnd,
"Auction already ended."
);
// 如果出价不够高,返还你的钱
require(
msg.value > highestBid,
"There already is a higher bid."
);
if (highestBid != 0) {
// 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数,
// 是有安全风险的,因为它有可能执行一个非信任合约。
// 更为安全的做法是让接收方自己提取金钱。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// 取回出价(当该出价已被超越)
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `send` 返回之前,重新调用该函数。
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
// 这里不需抛出异常,只需重置未付款
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnd() public {
// 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币),
// 一个好的指导方针是将其结构分为三个阶段:
// 1. 检查条件
// 2. 执行动作 (可能会改变条件)
// 3. 与其他合约交互
// 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态,
// 或者导致某些效果(比如支付以太币)多次生效。
// 如果合约内调用的函数包含了与外部合约的交互,
// 则它也会被认为是与外部合约有交互的。
// 1. 条件
require(now >= auctionEnd, "Auction not yet ended.");
require(!ended, "auctionEnd has already been called.");
// 2. 生效
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. 交互
beneficiary.transfer(highestBid);
}
}
bid()
函数有两个参数 public
和 payable
。关键字public让这些定义的代码可以从外部读取。 payable关键字标明该函数/合约是否接受外部转账,以太坊中规定,如果一个函数可以接收外部转账,则必须标记为payable
。
调用合约
外部账户调用合约,如同外部账户间的交易一般。
基于直接调用方式进行,如果被调方异常,调用方也会触发异常进行回滚操作。
基于call函数的方式进行,如果调用过程中被调方异常,会导致call()返回false,但发起调用的函数不会抛出异常,而是继续执行。
基于deletatecall方式进行,错误处理方式和call函数方式一致,只是执行环境不同,deletatecall方式会在调用方自己的环境中执行。
fallback函数是一个函数调用的缺省值。当外部账户调用合约,但是不在data域中说明调用哪个函数或者该调用函数在合约中不存在,则会执行缺省调用fallback
。如果没有fallback
则合约会抛出异常。
合约创建和运行
汽油费
智能合约是一个图灵完备的编程模型Turing-complete Programming Model
,基于此,代码编写过程中可能产生死循环,如何解决在调用合约过程中产生的死循环呢? gas fee
汽油费出现了。执行合约中的指令需要收取汽油费,由发起交易的人来支付。且不同的指令消耗的汽油费时不一样的,一般简单的指令很便宜,复杂或者需要存储状态的指令很昂贵。若发起交易时汽油费总量不足以支付调用过程中产生的汽油费消耗,则会发起回滚。
ETH的Block Header结构,其中GasUsed
和GasLimit
分别表示该区块所有交易消耗汽油费的使用总量以及该区块能够消耗汽油费的上限。GasLimit
如同BTC系统中的区块大小限制1M
一般,用来限制区块,限制区块过大导致系统负载变大。但是与BTC系统不同的是,BTC区块大小限制1M
是写死的,作为ETH的矿工,却可以动态调整区块的GasLimit
大小,其调整幅度为前区块的 1/1024
,如此操作,当区块链系统的矿工普遍认为GasLimit
太小或者太大,即可立即调整,使GasLimit
趋向一个普遍共识的平均值。
// Header represents a block header in the Ethereum blockchain.
// https://github.com/ethereum/go-ethereum/blob/master/core/types/block.go
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
}
错误处理
以太坊的错误处理具有原子性,一个交易要么全部执行要么全部不执行。交易执行成功后,剩余的汽油费会退回给调用者;执行失败,触发回滚,但是消耗的汽油费不会退回给调用者,防止恶意攻击。
挖矿与执行的顺序
Q:假设全节点要打包一些交易到区块中,其中存在某些交易是对智能合约的调用。全节点应该先执行智能合约再挖矿,还是先挖矿获得记账权后执行智能合约?
A:先执行智能合约后挖矿,Block Header
结构有三棵树 Root -> stateRoot
TxHash -> transactionsRoot
ReceiptHash -> receiptsRoot
状态树、交易树、收据树。只有先将交易全部执行完,才能进行区块的组装并挖矿。如果先挖矿,再执行交易,则带来一个问题,交易会产生三棵树的变化,变化导致Block Header
信息发生变化,先前的挖矿白费。
挖掘一个新区块其实包括两部分,第一阶段组装出新区块的所有数据成员,包括交易列表tx、叔父区块uncles等,并且所有交易都已经执行完毕,各帐号状态更新完毕;第二阶段对该区块进行授勋/封印(Seal),没有成功Seal的区块不能被广播给其他节点。第二阶段所消耗的运算资源,远超第一阶段。[3]
Q:汽油费的燃烧机制?
A:首先,状态树、交易树、收据树这三棵树都位于全节点中,是全节点在本地维护的数据结构,记录了每个账户的状态等数据,所以该节点收到调用时,是在本地对该账户的余额减掉即可。所以多个全节点每人扣一次,仅仅是每个全节点各自在本地扣一次。 也就是说,智能合约在执行过程中,修改的都是本地的数据结构,只有在该智能合约被发布到区块链上,所有节点才需要同步状态,各自在本地执行该智能合约。
Q:智能合约支持多线程?
A:不支持,solidity不支持多线程语句。因为以太坊本质为一个交易驱动的状态机,这个状态机必须是完全确定性的,给定一张智能合约,面对同一组输入,产生的输出必须转移到一个完全确定的状态。对于多线程,多核访问顺序不同,即同一组输入的输入顺序不同,产生的输出结果可能不同。
地址类型
Address
是Solidity的一个数据类型。以太坊中的地址的长度为20字节,一字节等于8位,一共160位。由于钱包地址是以16进制的形式呈现,160 / 4 = 40
,所以钱包地址的长度为40。
address.balance
address这个账户的余额
address.transfer(12345)
,非是address向其他人转账12345Wei,因为没有收款人的address。其实该函数指的是当前合约向address地址中转入了12345Wei。
三种发送ETH的方式
<address>.transfer(uint256 amount)
<address>.transfer(uint256 amount)
<address>.transfer(uint256 amount)
transfer
在转账失败后会导致连锁性回滚,即被调用者异常,调用者也将抛出异常;
send
转账失败会返回false,不会导致连锁性回滚。
call
的方式本意是用于发动函数调用,但是也可以进行转账。
transfer和send调用时,只发生2300wei的汽油费,汽油费的多寡,决定了调用功能的多样性,这里2300是一个非常小的费用,可能只能调用写log的功能。call的方式常用在合约调用合约的嵌套中,调用合约者不知道被调合约要执行哪些操作,为了防止汽油费不足导致交易失败,通过call调用将自己所有的汽油费转交给被调合约来减少失败可能性,剩余的汽油费会返还给调用者。
重入攻击
结束了吗?
参考
引用1:蔡维德:判断智能合约真伪有这3个重要原则 引用2:solidity develop 文档 引用3:ETH挖矿流程 ETH智能合约