以太坊开发实战(第1部分:智能合约)

时间:2022-05-05
本文章向大家介绍以太坊开发实战(第1部分:智能合约),主要内容包括所以,以太坊到底是什么?、什么是智能合约?、在开始开发智能合约之前、开发智能合约、使用IDE、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

智能合约(smart contracts),ICOs, Mist, Metamask, Remix, geth, web3...如果您愿意花一点时间在以太坊的开发上面,相信这些单词对您来说可能并不陌生。

一些人将智能合约部署到测试网络,其他人则告诉您应该阅读以太坊的黄皮书,而另一些人则建议您使用 Truffle 套件因为它很好用。您作为一个 “萌新” 待在这群 “大神” 周围,不知道自己到底应该如何开始使用以太坊,更对如何一起协同工作,如何构成以太坊一无所知。

如果本文是您在以太坊或区块链生态系统上读到的第一篇文章,那么您将会爱上这个系列。专家们在Twitter上进行互动,高谈阔论着那些不安全的标准及协议,以及未经侧且bug横流的开发工具。然而并不是区块链中的所有东西都已经被搞定,每个人都在朝着不同的方向前进,因为还有许多工作需要完成。研究机构,银行和政府的未来正由 “疯狂” 的开发者决定!这听上去太棒了。

不管怎样,别再担心了,我会尝试在本系列教程中为您关联所有的知识点,带您穿越智能合约和去中心化应用开发的宇宙,向您展示它们是如何运作的。

虽然我不会详细讨论以太坊中的每一个细节,但我会把一些有助于更好理解概念的材料链接发给您,您可以自行决定是否需要深入研究它们的全部细节。本系列教程的目标是以最通俗的方式,帮助您更好地理解以太坊背后的原理,仿佛您某个热衷于该技术的朋友,向您娓娓道来。

所以,以太坊到底是什么?

以太坊的官方网站给出这样的解释:

以太坊是一个运行智能合约的去中心化平台:平台上的应用程序完全按照原有的程序运行,不存在停机,审查,欺诈或第三方干扰的可能性。

换句话说,以太坊使用了区块链技术来验证我们运行的代码执行的功能。

如果您对区块链,以太坊,比特币,加密货币或者其他类似词的意思一无所知,我建议您可以去收听 Tim Ferriss 精彩的播客,他在节目里采访了 Nick Szabo 和 Naval Ravikant:The Quiet Master of Cryptocurrency - Nick Szabo

什么是智能合约?

以太坊中的智能合约是可以处理金钱交易的脚本。就这么简单。

这些合约是由被我们称为“矿工”的当事人强制执行的。“矿工”代表多台计算机,他们负责将一笔交易(执行智能合约,支付加密货币等)添加到被称之为“区块”的公共分类账上。多个区块构成一个区块链。

我们付给这些矿工一种叫做Gas的奖励,这也是执行一份合约的成本。当您发布一项智能合约,或者执行了其某项功能,再或者将数字货币转到其他账户时,您需要支付一些货币作为 Gas,也就是处理交易的报酬。

如果还不清楚什么是智能合约,或者想了解更多细节,可以点击下列的链接:

在开始开发智能合约之前

在本教程中,我假设读者您已经有了一些软件开发的基础,并且具备了Javascript 和Node.JS 的基本知识。否则,您很快就会迷失在一堆语法中。Solidity 是我们开发智能合约的编程语言,它与Javascript的语法非常接近,由于许多以太坊的开发工具都基于 Javascript 和 NodeJS,所以如果您已经熟悉了这两种语言,那么您将更容易上手。这不是针对初学者的教程,如果您不了解某些概念,我会为您提供一些基础教程的链接,但是否继续学习这篇教程是由您自己来决定的。

开发智能合约

终于,我们开始了本教程真正有趣的部分,正如我之前所说,Solidity 与 Javascript 很接近,但它们并不相同。我很遗憾地说,熟悉前端语言的开发者们,您不能在一段 Solidity代码上使用 JQuery,并还希望它能运行,您不能忽视安全问题,不能在安全性问题上草草了事。我将会在我们讨论安全性,开发模式以及防守代码时解释具体原因。

那么现在,让我们来看本教程中的第一个例子,我准备采用电影“In Time”中的某段情节,在反乌托邦的未来,人们把他们剩余的时间换成金钱,用来维持生计。在电影中,描述了一种将时间作为赌注的博弈游戏,我们也来这样做——用一份智能合约来博弈。

文章中没有附上我的代码,不过不用担心,我将在Github上提供我所提到的所有脚本。(译者注:代码已经置入文章)

另外,如果这是您第一次和智能合约打交道,如果您现在还不能掌握所有内容,这很正常。您需要多练习,多阅读文档,以及多做一些研究来适应 Solidity。

代码解说

现在,(敲黑板)让我们来看代码。首先,Solidity 脚本的基础是以下片段:编译指示告诉编译器我们正在使用哪个版本的 Solidity,以及我们合约的名称,这与 Javascript 中类的结构相似。这就是所谓的“博弈”。

pragma solidity ^0.4.18;
​
    /**
    * Example script for the Ethereum development walkthrough
    */
​
contract Wrestling {
    /**
    * Our wrestlers
    */
    address public wrestler1;
    address public wrestler2;
​
    bool public wrestler1Played;
    bool public wrestler2Played;
​
    uint private wrestler1Deposit;
    uint private wrestler2Deposit;
​
    bool public gameFinished; 
    address public theWinner;
    uint gains;
​
    /**
    * The logs that will be emitted in every step of the contract's life cycle
    */
    event WrestlingStartsEvent(address wrestler1, address wrestler2);
    event EndOfRoundEvent(uint wrestler1Deposit, uint wrestler2Deposit);
    event EndOfWrestlingEvent(address winner, uint gains);
​
    /**
    * The contract constructor
    */
    function Wrestling() public {
        wrestler1 = msg.sender;
    }
​
    /**
    * A second wrestler can register as an opponent
    */
    function registerAsAnOpponent() public {
        require(wrestler2 == address(0));
​
        wrestler2 = msg.sender;
​
        WrestlingStartsEvent(wrestler1, wrestler2);
    }
​
    /**
    * Every round a player can put a sum of ether, if one of the player put in twice or 
    * more the money (in total) than the other did, the first wins 
    */
    function wrestle() public payable {
        require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2));
​
        if(msg.sender == wrestler1) {
            require(wrestler1Played == false);
            wrestler1Played = true;
            wrestler1Deposit = wrestler1Deposit + msg.value;
        } else { 
            require(wrestler2Played == false);
            wrestler2Played = true;
            wrestler2Deposit = wrestler2Deposit + msg.value;
        }
        if(wrestler1Played && wrestler2Played) {
            if(wrestler1Deposit >= wrestler2Deposit * 2) {
                endOfGame(wrestler1);
            } else if (wrestler2Deposit >= wrestler1Deposit * 2) {
                endOfGame(wrestler2);
            } else {
                endOfRound();
            }
        }
    }
​
    function endOfRound() internal {
        wrestler1Played = false;
        wrestler2Played = false;
​
        EndOfRoundEvent(wrestler1Deposit, wrestler2Deposit);
    }
​
    function endOfGame(address winner) internal {
        gameFinished = true;
        theWinner = winner;
​
        gains = wrestler1Deposit + wrestler2Deposit;
        EndOfWrestlingEvent(winner, gains);
    }
​
    /**
    * The withdraw function, following the withdraw pattern shown and explained here: 
    * http://solidity.readthedocs.io/en/develop/common-patterns.html#withdrawal-from-contracts
    */
    function withdraw() public {
        require(gameFinished && theWinner == msg.sender);
​
        uint amount = gains;
​
        gains = 0;
        msg.sender.transfer(amount);
    }
} 

我们想要两个 wrestler,所以我们将设置两个变量来保存他们的账户地址(他们的钱包的公钥)。 (译者注:wrestlers:参与博弈的人)

在我们的小游戏中,wrestlers 在每轮比赛中都能够投入一笔钱,如果其中一方累计投入资金是另一方的两倍,那么他将赢得游戏。

关键词 private/public 其中一个重要的点是,即使一个变量被设置为 private,并不意味着其他人无法获得其中的内容,而是表明它只能在合约内部访问。由于整个区块链存储在很多计算机上,因此存储在变量内的信息总是可以被其他人看到,这点表示您始终应该牢记安全第一。

另一方面,编译器会自动为 public 变量创建 getter 函数。为了使其他合约或用户可以更改 public 变量的值,您需要手动创建一个 setter 函数提供修改变量的入口。

现在我们为游戏中的每一轮添加三个可能的事件。开始事件,当 wrestler 注册时触发,在游戏期间,每轮比赛会触发比赛事件,最终当 wrestler胜出时,会触发第三个事件,结束事件。所有事件的内容只是记录日志,在分布式应用 dapps 的开发中,事件可以设置为调用具有实际功能的 JavaScript 函数。在开发过程中,甚至可以将事件用于调试,因为在 Solidity 上没有类似与 JavaScript 的 console.log()函数提供打印功能。

接下来我们为 Solidity 添加构造函数,构造函数与我们的合约名称相同,并且只在创建合约时调用一次。

第一位 wrestler 将是创造合约的一方。“msg.sender” 是该函数调用者的地址。

接下来我们为另一个 wrestler 注册以下函数:

require 函数是 Solidity 中一个特殊的错误处理函数,如果某个条件没有得到满足,它将恢复之前的更改。在我们的例程中,如果变量 wrestler2 等于 0x0 地址(相当于地址 0),我们将添加还未创建的 wrestler2 变量,如果 wrestler2 的地址不等于 0x0,则意味着 wrestler2 已经被注册为对手,因此,我们将不再需要继续注册新的对象。

再重复一遍,“msg.sender” 是该函数调用者的地址,我们发出一个事件来表示博弈开始。

现在,每个 wrestler 都会调用一个函数,wrestle(),投入一定量的金钱。如果双方都参加了比赛,我们会看看他们中的一个是否赢得了这场比赛(赢得比赛的规则是其中一方累计投入资金是另一方的两倍)。“payable”关键字表示该函数可以收取数字货币,如果没有设置该关键词,那么该函数不会接受以太币。而 “msg.value” 是合约中以太币数量。

接下来我们添加 endOfGame()和 endOfRound()函数。其中用到的关键字 internal 与 private 作用类似,唯一的区别是用 internal 修饰的函数可以被其他合约继承(因为Solidity与其他面向对象语言相似),而 private 修饰的函数则不能被继承。

有一点需要注意,我们并没有直接向赢家转账资金,在这个例子中,直接与否并不重要,因为胜者将拿走合约中所有的资金,在其他更一般的情况下,当多个用户可以从合约中提取以太币时,使用取款模式是一种更安全的方式,以避免多次重复提取。

简单地说,如果多个用户可以从合约中提取资金,某个用户可以简单地多次调用支付函数获取多倍于他所应得的资金。所以我们需要以例子中的方式构建我们的提款函数,防止上述情况出现。

您可以在以下链接找到整个代码片段:(译者注:或者上文中)

代码

使用IDE

您现在就可以复制上述的代码,然后粘贴到 Remix IDE 中,您可以在浏览器新标签页中打开这个 IDE

您可以直接在您的浏览器上使用 Remix,Remix 有很多您在其他地方几乎无法找到的有趣的功能。

点击页面左上角的按钮 “+”,然后创建一个名为 “Wrestling.sol” 的新文件,然后粘贴上面链接中的github repo的代码:

IDE 支持语法突出显示。而 Github上暂不支持 Solidity .sol 扩展。

在页面的右侧,您可以找到多个有趣的标签,例如“分析”标签,通过它可以显示错误和建议。我会建议您自己去尝试这个工具的功能。尽管我们将在下一部分中使用其他的工具,但正如我之前所说的,本系列教程的目标是为您连接各个知识点,并告诉您它们是如何结合在一起工作的,您可以自行按照喜好决定是否在浏览器上使用 IDE。如果您喜欢,欢迎您阅读Remix文档

或者,您可以通过阅读 solidity文档熟悉一下 。

在下一部分中,我们将看到如何将智能合约部署到两种测试网络,了解 truffle,ganache 和 geth,以及它们如何协同工作

如果您喜欢这第一部分,您可以在twitter上 @dev_zl找到我。

第二部分传送门在这里