区块链开发实战:如何从零打造一个去中心化应用

编译 | 区块链大本营

整理 | reason_W

区块链会是一场革命吗?

今天,很多成功的互联网企业都是在以中介者的形式存在。比如Google——你和互联网之间的中介,比如亚马逊——买家和卖家之间的中介。但区块链——以“去中心化”为最基本特征的技术的发展却很有可能破坏这些互联网巨头赖以成功的结构。

互联网巨头的事咱们暂且不操心,那么从一个开发者的角度呢?我们需要关心什么呢?作为一个开发者,我们该如何使用区块链构建应用程序? 我们可以在这么复杂的基础概念之上创造出非常易用的工具吗?这个工具的开发体验又有多友好呢?

我们知道,最好的教程就是带你去从头开发一个应用程序。所以我们将使用区块链技术制作一个简单的去中心化广告服务器,称为“零美元主页”。本文将告诉你一群开发者在摸索使用区块链技术时最完整的试错经过和心路历程

什么是区块链应用?

要做区块链应用,首先要知道区块链有什么特性。而它最突出的特性就是“去中心化”。简单来说,就是不再像以前的技术一样需要一个中心节点负责管理,而是通过一系列复杂且可靠的技术,由网络中所有的节点共同认可,共同记录,共同维护,解决网络中交易双方的信任问题。这是一条神奇的链,有了这个链就真的可以实现没有中间商赚差价了。

回到正文,区块链正在以去中心化的特质威胁着互联网巨头们的基础。而本文将集中于广告平台。广告平台是广告位购买方和广告渠道提供商之间的中介。我们的项目是通过区块链技术建立一个去中心化的广告平台。也就是跳过广告中间商,通过利用区块链技术本身的特性解决在互联网环境中进行交易时相互之间的信任问题。

自著名的百万美元主页实验以来,想要在付费广告领域通过创新变的富有已经越来越困难了。

The Million Dollar Homepage http://www.milliondollarhomepage.com/

2005年,一个21岁的英国男孩Alex创建了一个名为“百万美元主页”的网站,将页面做成100万个小格子组成的广告牌,每个格子售价1美元。这一别出心裁的创意吸引了不少广告商注意,不到一年时间内,100万个格子全部售出。

相反,我们选择建立一个允许免费展示广告的平台——零美元主页。 免费,但并不代表什么都不付出:广告商需要通过开源贡献来换取广告位。因此,我们构建了一个去中心化的应用来管理广告在特定网页上的展示方式。 广告商需要具备编程能力,以便能够将他们的广告放在此页面上。

用户工作流程

具体而言,当我们在marmelab的开源库(https://github.com/marmelab)之一合并Pull请求(PR)时,会有一个GitHub机器人对PR进行评论,并邀请PR作者在广告平台管理面板上发布他们的广告。

打开评论中包含的链接之后,PR作者会被要求使用他们的GitHub凭证进行登录。然后,他们可以上传广告——实际上就是一张简单的图片。此图片将按照时间顺序添加到同样由其他PR作者上传的图片队列中。

每天半夜的时候,脚本都会自动地获取下一个图像(使用先进先出排序),并在接下来的24小时内将其显示在http://marmelab.com/ZeroDollarHomepage/ 上。

注意:这整个过程不需要中介参与,但为了避免在网站上出现成人内容,我们会在图像上传之后,发布之前通过Google Vision API进行验证。

应用架构

以下是我们广告平台的四步的架构:

  • 开源贡献者通知 只要开源PR合并到了我们的某个存储库中,GitHub就会将PR详细信息发送给负责管理的app。app会在PR下方进行评论以通知贡献者。该评论包含一个可以进入管理界面的链接,以及PR的详细信息。
  • 声明和图片上传 在打开评论链接之后,贡献者会转到管理员界面。他必须使用他的GitHub凭证登录才能通过身份验证。管理程序然后会通过GitHub获取PR详细信息,并检查贡献者是否是PR作者。如果没有问题,管理程序会显示图片上传栏。当贡献者上传图片时,管理程序会将PR ID推送到区块链,并将图片上传到CDN(以PR id命名)。管理程序同时会根据区块链中仍在等待发布的图像的有效PR数量计算并显示图像发布的大致日期。
  • 广告位置 每隔24小时,cron(一个执行周期性任务计划的程序)就会要求区块链查找尚未发布的下一个PR。 区块链将此PR标记为已发布并发送该ID。 cron将以PR id命名的图像重命名为“current image”。
  • 广告显示 每次访问者想要在“零美元主页”中发布广告,该主页都会向CDN询问当前图像。如果它恰好是区块链上最新发布的广告,那么该广告就会至少保留1天(直到另一个贡献者声明了一个PR为止)。

看起来有点意外的是,区块链在这个过程中起着非常小的作用。这是因为在实际编程过程中,我们发现无法把广告平台的整个代码都建立在区块链之上。事实上,区块链在连接互联网和处理方面的能力都非常有限。所以我们只用区块链来实现最关键的广告投放任务:

  • 注册经过认证的贡献者的Pull请求
  • 获取最后一次没有发布的Pull请求,并将其标记为发布

由于各种原因,管理程序中的其他任务最终没有使用区块链:

  • 从webhook对Pull的请求进行注册 在Pull请求之前进行注册是没有用的,因为Pull作者可能从未声明它。此外,将数据存储在区块链中并不是没有代价的,所以我们只存储我们必须存储的数据。但这样做的缺点是,我们的公共软件仓库(包括在此实验之前创建的公共软件仓库)中的任何公共资源都有资格进行下一步操作。
  • 通过在GitHub发布评论来通知用户 智能合约不能调用外部API,所以我们不能通过这项技术来实现本项任务。因此,我们将此任务交给管理程序来完成。
  • 验证声明的PR的作者 同样是因为智能合约不能调用GitHub API。所以,我们也把这个逻辑任务移至管理程序,并将其作为调用区块链的先决条件。
  • 存储图像 理论上,我们可以将任何东西存储在区块链中,包括图像。 但实际而言,图像需要很多存储空间,而且我们没有办法在我们的智能合约中存储多个“表格”(数据阵列)。
  • 将显示的广告更新为队列中的下一个 区块链中没有与setTimeout函数或cron工具等类似的功能。然而,你可能每隔x个区块就需要执行一次代码,不过这与时间无关。所以,我们在API上使用了类似cron的库。

研究,记录和第一次尝试

考虑到区块链网络的成熟度和设计目的,我们在选择要开发的区块链网络时最终选择了以太坊。

很快,我们就遇到了第一个困难。直到几个星期前,哪怕只是简单的测试,如果没有购买以太币,我们也无法使用以太坊区块链。此外,以太坊在之前的版本(名为Frontier)中并没有真正许可私有链,这就使开发变得非常复杂。因为没有私有链,只能在公共网络上进行开发,任何访问以太坊网络的人都可能会调用你的测试合约。更重要的是,这份文件只是一个志愿倡议,并且与发展状态不同步。

注:在我们开始开发程序后,以太坊就发布了新版本,从Frontier切换到Homestead,并且改善了Homestead的文档质量。

Homestead http://www.ethdocs.org/en/latest/introduction/contributors.html

尽管存在这些缺陷,我们仍然在Nancy,Paris和Dijon的以太网网络上成功注册了三个节点,并在这些节点之间共享ping。

在文档搜索中,我们最终找到了 Eris 文档(https://docs.erisindustries.com/)。Eris在解释区块链和合约方面做得非常出色。此外,他们专门在以太坊之上建立了一个层级,并开源了一系列工具以简化智能合约的开发过程。

Eris是一个命令行工具,你可以使用它来初始化你需要的任意数量的本地区块链。

如何操作智能合约

智能合约与API非常相似。它有几个公共函数,可以被在区块链网络上注册过的任何人调用。但与API不同的是,智能合约不能调用外部Web API(区块链是封闭的生态系统)。但是,智能合约可能会调用其他智能合约,只要知道他们的地址。

与API一样,公共函数只是它们的冰山一角。实际上合约可能由许多私有函数,变量等组成。

智能合约按照以太坊专有的二进制格式托管在区块链中,可由以太坊虚拟机执行。用于编写合约的语言和编辑器有:

  • Serpent,跟Python很像(https://github.com/ethereum/wiki/wiki/Serpent)
  • Solidity,跟Javascript很像(http://solidity.readthedocs.org/en/latest/)

在marmelab中,我们已经使用Javascript编写了很多代码,因此在编写合约时,我们选择了与Javascript类似的Solidity。Solidity合约存储在.sol文件中。

“零美元主页”合约

“零美元主页”合约存储了已声明的Pull请求,以及待发布的请求队列。 Solidity合约的第一个版本如下所示:

// in src/ethereum/ZeroDollarHomePage.sol
contract ZeroDollarHomePage {
    uint constant ShaLength = 40;
    enum ResponseCodes {
        Ok,
        InvalidPullRequestId,
        InvalidAuthorName,
        InvalidImageUrl,
        RequestNotFound,
        EmptyQueue,
        PullRequestAlreadyClaimed
    }
    struct Request {
        uint id;
        string authorName;
        string imageUrl;
        uint createdAt;
        uint displayedAt;
    }
    // what the contract stores
    mapping (uint => Request) _requests; // key is the pull request id
    uint public numberOfRequests;
    uint[] _queue;
    uint public queueLength;
    uint _current;
    address owner;
    // constructor
    function ZeroDollarHomePage() {
        owner = msg.sender;
        numberOfRequests = 0;
        queueLength = 0;
        _current = 0;
    }
    // a contract must give a way to destroy itself once uploaded to the blockchain
    function remove() {
        if (msg.sender == owner){
            suicide(owner);
        }
    }
    // the following three methods are public contracts entry points
    function newRequest(uint pullRequestId, string authorName, string imageUrl) returns (uint8 code, uint displayDate) {
        if (pullRequestId <= 0) {
            // Solidity is a strong typed language. You get compilation errors when types mismatch
            code = uint8(ResponseCodes.InvalidPullRequestId);
            return;
        }
        if (_requests[pullRequestId].id == pullRequestId) {
            code = uint8(ResponseCodes.PullRequestAlreadyClaimed);
            return;
        }
        if (bytes(authorName).length <= 0) {
            code = uint8(ResponseCodes.InvalidAuthorName);
            return;
        }
        if (bytes(imageUrl).length <= 0) {
            code = uint8(ResponseCodes.InvalidImageUrl);
            return;
        }
        // store new pull request details
        numberOfRequests += 1;
        _requests[pullRequestId].id = pullRequestId;
        _requests[pullRequestId].authorName = authorName;
        _requests[pullRequestId].imageUrl = imageUrl;
        _requests[pullRequestId].createdAt = now;
        _queue.push(pullRequestId);
        queueLength += 1;
        code = uint8(ResponseCodes.Ok);
        displayDate = now + (queueLength * 1 days);
        // no need to explicitly return code and displayDate as they are in the method signature
    }
    function closeRequest() returns (uint8) {
        if (queueLength == 0) {
            return uint8(ResponseCodes.EmptyQueue);
        }
        _requests[_queue[_current]].displayedAt = now;
        delete _queue[0];
        queueLength -= 1;
        _current = _current + 1;
        return uint8(ResponseCodes.Ok);
    }
    function getLastNonPublished() returns (uint8 code, uint id, string authorName, string imageUrl, uint createdAt) {
        if (queueLength == 0) {
            code = uint8(ResponseCodes.EmptyQueue);
            return;
        }
        var request = _requests[_queue[_current]];
        id = request.id;
        authorName = request.authorName;
        imageUrl = request.imageUrl;
        createdAt = request.createdAt;
        code = uint8(ResponseCodes.Ok);
    }
}

在第一次尝试时,我们使用Eris JS库与我们的区块链进行通信。从Node.js文件中解析合约就变得非常简单:

import eris from 'eris';
function getContract(url, account) {
    const address = // Read address file stored on disk by the eris CLI;
    const abi = // Read abi file stored on disk by the eris CLI;
    const manager = eris.newContractManagerDev(url, account);
    return manager.newContractFactory(abi).at(address);
}

而且它的调用也不困难:

function* newRequest(pullrequestId, authorName, imageUrl) {
    const contract = getContract(url, account);
    // First gotcha, when a function returns several named variables, they are returned as an Arrays
    // Second gotcha, numbers are returned as instances of BigNumber, do not forget to convert when standard numbers are expected
    const [codeAsBigNumber, displayDateAsBigNumber] = yield contract.newRequest(pullrequestId, authorName, imageUrl);
    const code = codeAsBigNumber.toNumber();
    if (code !== 0) {
        throw new Error(getErrorMessageFromCode(code));
    }
    // Return the displayDate for UI confirmation screen
    return displayDate.toNumber();
}

有关Eris JS库的更多信息,可以参阅Eris文档(https://docs.erisindustries.com/)。

单元测试合约

我们非常喜欢测试驱动开发,但遇到的第一个问题就是:我们如何测试Solidity智能合约?

Eris的项目成员也为此提供了一个工具:sol-unit(https://github.com/smartcontractproduction/sol-unit)。 它在Docker容器中(确保每次测试都在干净的环境中运行)为每次测试运行一个新的本地区块链网络,并执行测试。测试也会写成一个合约。

当然,这个工具也没那么快就能用。 sol-unit是一个npm软件包,为了使用测试功能(assertions,等),我们必须在测试合约中导入由这个软件包提供的合约。这可以用一个简单的Solidity语法实现:

import "../node_modules/sol-unit/.../Asserter.sol";

实际的编译过程并没有看起来那么顺利。编译合约时我们遇到了一个奇怪的情形。即,不能用上面这样的路径导入合约。 我们最终在测试的makefile目标中添加了一个命令,将这些sol-unit 合约复制到跟我们的项目相同的文件夹中。之后再运行sol-unit就很简单了,我们可以开始继续写代码了。

copy-sol-unit:
    @cp -f ./node_modules/sol-unit/contracts/src/* ./src/ethereum/
compile-contract:
    solc --bin --abi -o ./src/ethereum ./src/ethereum/ZeroDollarHomePage.sol ./src/ethereum/ZeroDollarHomePageTest.sol
test-ethereum: copy-sol-unit compile-contract
    ./node_modules/.bin/solunit --dir ./src/ethereum

运行测试区块链

运行区块链和部署我们的合约只要按照Eris文档就会非常简单。通过使用之前已经集成在makefile中的一些命令,我们又进一步解决了遇到的一些麻烦。基于我们的合约运行新区块链的整个过程如下所示:

  • 重置任何正在运行的eris docker容器,并删除一些临时文件
  • 启动eris密钥服务
  • 生成我们的账户密钥,并将其地址存储在一个便于稍后由JS API加载的文件中,
  • 生成genesis.json,这是区块链的“区块0”
  • 创建并启动新的区块链
  • 将合约上传至区块链并保存其地址,以便在需要时调用

几天的工作之后,我们就能够在本地的Eris区块链上运行合约了。

从Eris到以太坊

我们希望可以在本地以太坊区块链上尝试我们的合约。

要与以太坊区块链内的合约进行通信,我们必须使用Web3库。在尝试使用它们的过程中,我们也学到了很多,并意识到了Eris的许多潜在的复杂性。

首先,之前假设合约与API类似的想法是不正确的。我们必须区分仅从区块链读取数据的函数,以及将数据写入区块链的函数。

第一种(只读函数),像API所做的那样,会异步返回结果数据。第二种(写入函数)只会返回一个交易散列。并且在相应的块被开采之前(这可能需要一些时间,最差情况是在10秒到1分钟之间),写入函数的预期副作用(区块链内部的更改)不会生效。另外,让这些写入函数返回值的能力我们也还没有。所以我们不得不改变我们的solidity代码来首先调用写入函数,然后调用只读函数来获得结果。

我们还发现了一些事件,可以在智能合约中发生情况时进行通知。智能合约负责触发事件。它们在solidity的代码可以这样写:

event PullRequestClaimed(unit pullRequestId, uint estimatedDisplayDate);

他们可以在任何智能合约函数中触发,例如:

PullRequestClaimed(pullRequestId, estimatedDisplayDate);

这些事件会被永久存储在区块链中。这意味着我们可以使用区块链存储事件。这可能是判断函数调用是否已成功执行的最简单方法:智能合约可以在其进程结束时触发事件,触发条件包括执行失败、计算结果等等。值得注意的是,Meteor 的某些集成包已经可以直接用了。

最终,虽然实现的功能几乎相同,但我们已经可以将我们的智能合约重构的相当简单。我们必须摆脱映射(这些映射无法使用——因为我们的交易不是由以太坊网络开采的)。

solidity这种语言可能和JavaScript很接近,但它仍然非常年轻,并且不完整。数组对象还没有我们在JavaScript中使用的那些函数(甚至没有indexOf),字符串对象也没有任何函数。这在不久的将来或许可以通过社区的贡献来解决。

以太坊的操作如下:

// in src/ethereum/ZeroDollarHomePage.sol
contract ZeroDollarHomePage {
    event InvalidPullRequest(uint indexed pullRequestId);
    event PullRequestAlreadyClaimed(uint indexed pullRequestId, uint timeBeforeDisplay, bool past);
    event PullRequestClaimed(uint indexed pullRequestId, uint timeBeforeDisplay);
    event QueueIsEmpty();
    bool _handledFirst;
    uint[] _queue;
    uint _current;
    address owner;
    function ZeroDollarHomePage() {
        owner = msg.sender;
        _handledFirst = false;
        _current = 0;
    }
    function remove() {
        if (msg.sender == owner){
            suicide(owner);
        }
    }
    function newRequest(uint pullRequestId) {
        if (pullRequestId <= 0) {
            InvalidPullRequest(pullRequestId);
            return;
        }
        // Check that the pr hasn't already been claimed
        bool found = false;
        uint index = 0;
        while (!found && index < _queue.length) {
            if (_queue[index] == pullRequestId) {
                found = true;
            } else {
                index++;
            }
        }
        if (found) {
            PullRequestAlreadyClaimed(pullRequestId, (index - _current) * 1 days, _current > index);
            return;
        }
        _queue.push(pullRequestId);
        PullRequestClaimed(pullRequestId, (_queue.length - _current) * 1 days);
    }
    function closeRequest() {
        if (_handledFirst && _current < _queue.length - 1) {
            _current += 1;
        }
        _handledFirst = true;
    }
    function getLastNonPublished() constant returns (uint pullRequestId) {
        if (_current >= _queue.length) {
            return 0;
        }
        return _queue[_current];
    }
}

对Pull的请求进行声明并返回估计的显示日期的演变过程为:

// make a [transaction](https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethsendtransaction) call to our smart-contract write function
contract.newRequest.sendTransaction(pullrequestId, {
    to: client.eth.coinbase,
}, (err, tx) => {
    if (err) {
        throw error;
    }
    // wait for it to be mined using [code](https://github.com/ethereum/web3.js/issues/393) from [@croqaz](https://github.com/croqaz)
    return waitForTransationToBeMined(client, tx)
        .then(txHash => {
            if (!txHash) throw new Error('Transaction failed (no transaction hash)');
            // get its receipt which might contains informations about event triggered by the contract's code
            // this function might also check wether the transaction was successful by analyzing the receipt for ethereum specific error cases (insufficient funds, etc.)
            return getReceipt(client, txHash);
        })
        .then(receipt => {
            // parse those logs to extract only event data
            return parseReceiptLogs(receipt.logs, contractAbi));
        })
        .then(logs => {
            if (logs.length === 0) {
                throw new Error('Transaction failed (Invalid logs)');
            }
            const log = logs[0];
            if (log.event === 'PullRequestClaimed') {
                // timeBeforeDisplay is a BigNumber instance
                return log.args.timeBeforeDisplay.toNumber();
            }
            if (log.event === 'PullRequestAlreadyClaimed') {
                const number = log.args.timeBeforeDisplay;
                if (log.args.past) {
                    // timeBeforeDisplay is a BigNumber instance
                    return number.negated().toNumber();
                }
                // timeBeforeDisplay is a BigNumber instance
                return number.toNumber();
            }
            if (log.event === 'InvalidPullRequest') {
                throw new Error('Invalid pull request id');
            }
        });
})

通过上面的代码,我们的去中心化应用就可以在本地以太坊网络中工作了。

部署到生产环境

如果说在本地环境中运行我们的应用是一项挑战,那么将其部署到真正的以太坊网络中的生产就是一场战斗。

这其中有几点非常值得注意。最重要的一点是合约在代码中是不可变的。这意味着:

  • 你部署到区块链的合约会永远保持在那里。如果你发现你的合约存在缺陷,那也无法修复——必须部署新的合约。
  • 部署现有合约的新版本时,以前合约中存储的任何数据都不会自动传输过去——除非你主动使用过去的数据初始化新合约。在我们的案例中,纠正合约中的错误实际上是抹去了已记录的PR(无论是已发布的广告还是待发布的广告)。
  • 每个合约版本都有一个id(例如,我们的“零美元主页”合约id是0xd18e21bb13d154a16793c6f89186a034a8116b74)。 由于过去的版本也可能包含数据,如果你不想丢失数据(我们也一样),请继续跟踪过去的合约ID。
  • 由于你无法更新合约,因此也无法回滚更新。在重新部署之前需要确保合约有效。
  • 当你部署现有合约的新版本时,旧的(错误的)合约仍然可以被调用。 任何引用合约的区块链以外的系统(例如我们在零美元主页中的节点管理应用)都必须更新为指向新合约。我们一开始忘了这么做,还非常困惑为什么我们的新代码没有运行,后来才明白。
  • 如果在代码中包含自杀(suicide )调用,合约作者可以终止合约。但合约的所有现有交易都会保留在区块链中——永远存在。如果不希望它消失,请确保终止开关已经处理了合约中的其余以太币。

还有一个问题是,区块链中的每个合约部署和写入操作都会产生数量不一的以太网络。我们设法获得了5个以太币,但并不知道到底需要多少以太币才能够部署我们的合约或者调用一个交易。要测试出来每次失败产生的成本是很难的。

对于Node.js部分,像前面大部分的项目一样,我们决定在AWS EC2实例上运行它。为此,我们必须:

  • 在服务器上运行以太坊节点
  • 将整个区块链下载到此服务器
  • 在节点上使用一些以太币解锁一个帐户
  • 部署我们的应用程序并将其链接到节点
  • 通过节点将我们的智能合约注册到区块链中

确保你的区块链节点服务器有充足的存储空间。目前区块链的大小约为15GB。 EC2实例的默认卷大小为8GB,确实也很大。因为一开始没有下载完整的区块链(我们也没有马上意识到),所以我们遇到了很多麻烦。例如,我们有一个有5个以太币的帐户,但很长一段时间,系统的状态都会显示我们没有解锁帐户,或者好像我们没有以太币。 直到我们将区块链的其余部分下载下来才解决了这个问题。

同样,解锁我们那个包含5个以太币的宝贵账户也不是一件容易的事。我们不想在应用程序中对我们的密码进行硬编码,而是想用supervisord运行节点来简化部署。艰辛的摸索之后(唉,累啊),我们终于找到了一种方式,可以在改变配置的同时,不会暴露密码。以下就是我们所用的supervisord配置:

[program:geth]
command=geth --ipcdisable --rpc --fast --unlock 0 --password /path/to/our/password/in/a/file
autostart=false
autorestart=true
startsecs=10
stopsignal=TERM
user=ubuntu
stdout_logfile=/var/log/ethereum-node.out.log
stderr_logfile=/var/log/ethereum-node.err.log

最后一个安全提示:区块链的远程过程调用(RPC)端口为8545。请勿在EC2实例上打开此端口!否则任何知道实例IP的人都可以控制你的以太坊节点,并窃取你的以太网。

以太币和瓦斯(Gas)

在以太坊区块链中部署和调用合约并不是免费的,需要承担一定的计算代价。而且由于区块链运行代价高昂,任何写入操作都要承担代价。在以太网中,调用写入合约的方法的代价取决于方法的复杂性。以太坊附带了一份瓦斯费用列表(http://ether.fund/tool/gas-fees),告诉你应该在合约调用时需要添加多少以太币才能让它执行。

实际上,这里的以太币消耗量其实非常小,只占一个以太币的一小部分。以太坊区块链还引入了另一种运行合约的货币:瓦斯(Gas)。

1瓦斯= 0.00001以太币

1 以太币= 100,000瓦斯

根据计算能力的供应情况和计算需求,未来瓦斯和以太币的转化率会有所不同。

处理一次交易的费用并不是强制收取的,但却非常推荐。以太坊的文件中说:“矿工可以选择忽略瓦斯价格太低的交易”。成功开采好一个新区块会给矿工奖励5个以太币。

为了调用我们自己的合约,以太坊区块链大概需要0.00045个到0.00098个以太币(这个实际价格取决于瓦斯价格和交易使用的瓦斯)。

那么怎样才能获得以太币和瓦斯呢?你可以购买以太币(当然主要是通过交换比特币获得),也可以自己去挖矿。在法国,比特币或以太币的购买需要的程序几乎和开设银行账户一样麻烦。这个过程很慢(大概要几天),也很痛苦,取决于由报价和需求确定的汇率。

挖矿以太币

最后我们决定自己挖掘以太币。如果想了解一下在以太坊挖矿到底是不是有利可图,自己挖确实也是一个很好的方法。我们制作了一个非常大的亚马逊EC2实例,它具有强大的GPU计算能力(是一个g2.2xlarge实例)。这个实例的价格是每天17美元。我们安装了以太网,并启动了我们的节点。由于高内存和存储需求,我们很快就必须增强这个实例。加入区块链时节点做的第一件事就是下载过去交易的全部历史记录。这需要大量的存储空间:区块链历史记录超过14GB,Ethash工作证明大约需要3GB。

一旦以太坊节点启动,我们就必须要挖掘3天才能创建出一个有效的区块:

提醒一下,以太坊区块链每10秒钟挖一块。开采一个区块可以获得5个以太币,售价大约为55美元(作者写文章时的价格)。我们的增强版EC2实例运行3天的成本约为51美元。总而言之,在AWS上挖以太币比直接买以太币更便宜。但是我们非常幸运:我们开发这个区块的时候挖掘难度并不很大,在开发完之后,网络的挖掘难度就增加了三倍。

5个以太币可以让我们运行“零美元主页”多长时间呢?现在我们来计算一下。

“零美元主页”的工作流程意味着每天都会有一笔交易,另外每个声明的PR都会有一笔交易。假设贡献者每天声明一个PR,那么运行该平台每年最多将花费365 * 2 * 0.00098 = 0.72 以太币。5个以太币可以让我们运行该平台近7年。

正如你所看到的,在以太坊运行合约并不是免费的,不过以目前的价格来说,它仍然很便宜。当然,以太币价值的变化很大。由于挖比特币的利润越来越低,一些大型比特币矿场开始转向以太坊。这也让采矿变得越来越困难,并且使得以太网每天都在变得更加昂贵。

最终的惊喜

最终,我们的智能合约在EC2上托管的现实世界以太坊节点中运行非常好。

但当我们完成这个项目的时候,以太坊发布了它们的Homestead版本,这带来了很多新东西,完全破坏了我们的代码。我们花了大约一个星期的时间才明白,并且通过反复试验修复了因不明原因而不兼容的代码。

Tip

Homestead发布了一个隐藏的以太坊功能——私有网络——来简化开发。之前以太坊缺乏私有网络是我们当时选择使用Eris的原因之一。

“零美元主页”平台现在已经启动并且开始运行了。你可以通过在GitHub上的marmelab的开源库之一上开一个Pull请求来使用它,查看http://marmelab.com/ZeroDollarHomepage/ 上当前显示的广告,或浏览marmelab / ZeroDollarHomePage上的应用程序代码。是的,我们正在开源整个广告平台,以便你可以详细了解其工作原理,并在本地进行复制。

调试

以太坊留给开发者的体验其实是非常糟糕的。想象一下没有日志,也没有调试工具,你发现程序失败的唯一方法是通过一行一行输出“I'm here”字符串来查找问题。甚至有时(例如在Solidity合约中),你都不能这样做。或者某些在开发环境中完美工作的程序在生产环境中却无法实现。这就是以太坊的开发者体验。

如果你将数据存储在智能合约中,是没有内置的方式可以在交易后显示此数据当前状态的。这意味着你需要构建自己的可视化工具来排除错误。

可用于跟踪以太坊合约和交易的工具有:

  • etherscan.io:显示有关合约,交易,区块的数据
  • etherchain.org:区块和以太网信息
  • 你还可以获得有关网络和节点的汇总统计信息

例如,这是我们的合约在etherscan上的可视化界面:

每次交易(对合约方法的调用)以及合约执行的痕迹都会用机器语言记录下来。除了用于确保你调用到了合约之外,这个工具不能用于调试的其他部分。

而且,这些工具只能监视公共以太坊网络。所以你不能用它们来调试本地的区块链。

如果你曾经见过比特币交易审计网站,千万不要以为以太坊可以达到相同的复杂程度。此外,比特币网络只有一种交易,因此比设计用于运行智能合约的网络更容易监控。

文档

这还不是全部:以太坊文档与代码不同步(至少在Frontier版本中),所以大多数时候我们必须要通过查看这些库的源代码来了解如何写代码。由于有些出问题的库使用的语言(Solidity)很少人用,所以我们在这里只能祝福它们的工作方式不出问题了。还有,也不要指望Stack Overflow的帮助。像我们这样敢于做一些认真的事情来为社区提供支持的人太少了。

不过这里需要明确的是:我们不是在批评以太坊社区缺乏努力。以太坊背后的发展势头巨大,事情进展迅速。所有文档贡献者的工作都值得赞赏。但还是要承认在我们开发应用程序时,现有文档状态不足以让新的以太坊开发人员启动一个项目。

在网上搜索以太坊的教程很容易,但大多数时候,这些教程中复制粘贴的代码根本无法使用。

如果你想自己动手开发智能合约,以下是一些值得一看的资源:

  • A 101 Noob Intro to Programming Smart Contracts on Ethereum https://medium.com/@ConsenSys/a-101-noob-intro-to-programming-smart-contracts-on-ethereum-695d15c1dab4
  • 以太坊指南 https://ethereum-homestead.readthedocs.org/en/latest/
  • 以太坊 Github wiki https://github.com/ethereum/wiki

结论

经过两位经验丰富的开发人员4周的艰苦努力,我们的代码终于可以在公有以太坊网络中工作了(心累)。在Frontier和Homestead版本之间的以太坊库中的回归和兼容性中断也并没有起到什么作用。查看marmelab / ZeroDollarHomePage上的项目源代码可以详细了解其内部工作原理。因为确实是第一次开发,我们在这方面的经验也实在有限,请原谅我们代码中的潜在错误,以及本文中的不准确之处。请随时在GitHub向我们提交更正或评论。

我们不喜欢这段经历。通过糟糕的文档和不成熟的软件库摸索编程的方式并不是很让人开心。用半熟的语言来实现简单的功能(如字符串操作)也不好玩。尤其是意识到自己尽管有着多年丰富的脚本语言编程经验,但却无法编写简单的可靠合约,这就更令人沮丧。最重要的是,以太坊生态系统的年轻人完全无法预测他们实现一个简单的功能所需的时间。由于时间就是金钱,目前我们还无法确定开发去中心化应用到底需要多少代价。

在时间和资源方面,“零美元主页”代表着超过20,000欧元的开发成本——即使它是一个非常简单的系统。与我们在其他项目中使用的工具(Node.js,Koa,React.js,PostgreSQL等)相比,在区块链上开发非常昂贵。对于开发团队来说,这也是非常令人失望的。我们还可以从中发现一个很强烈的信号:这个生态系统还没有准备好!

对区块链的看法

在探索了区块链的理论,并真正开发之后,我们已经对它的优缺点有了切身的体会。但令人惊讶的是,我们的大部分结论都和媒体上一直吹捧的不太一样。或许这是因为我们并没有迷信比特币和其他人的疯狂估值,也可能是因为区块链确实面临着不够成熟的现状。

区块链确实是一个非常聪明的想法,具有巨大的潜在影响。但是目前的方案究竟能否为下一个十年颠覆性应用的诞生提供动力还尚未可知。

在技术方面,它的一些基本特征根本不可行。区块链效率不够高,对开发人员不够友好,而且它的技术特性很有可能被恐怖分子或地下黑市利用,用于非法毒品、武器、人口等的贩卖,而难以监管。

在商业方面,区块链变化速度过快,价格昂贵。费用可能会无缘无故地变化数十倍。在这样一个不稳定的平台上开展业务是非常危险的。

我的意思是,我们必须等待。区块链还没有准备好。它需要更成熟些,需要成为另一个杀手级应用而不是成为一个投机引擎,需要更大的开发者社区,需要承担更多的生态和经济责任。这需要多长时间?也许一年或两年?没人能说出来。

说实话,最后得出这样的看法也让我感到很惊讶。关于区块链的大部分出版物都表明了相反的情况。他们说“现在正是时候”,“不要错过火车”,或者“下一个十年的巨型企业正在区块链上建立”。也许他们是错的,也许我们是错的。我们正在试图用更有力的证据来论证这一分析。如果您有不同的意见,欢迎和我们沟通交流。我们将密切关注不同区块链项目的发展。

原文: https://marmelab.com/blog/2016/05/20/blockchain-for-web-developers-in-practice.html

原文发布于微信公众号 - 区块链大本营(blockchain_camp)

原文发表时间:2018-03-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏钱塘大数据

中国首个区块链标准《区块链参考架构》发布

作者:数据观 ? 2017年5月16日上午,在杭州国际博览中心举行的区块链技术应用峰会暨首届中国区块链开发大赛成果发布会上,首个区块链标准《区块链 参考架构》...

49880
来自专栏区块链大本营

实战必读! 发现百万级用户公链漏洞,我们靠的是这套方法

本文作者负责支撑工信部、发改委、央行、雄安新区及多省市区块链产业落地,保障场景建设质量。受托测试的公链是国际两大楔入式侧链技术解决方案之一,拥有注册用户百万级。...

8610
来自专栏云计算D1net

区块链安全如何保证?公有私有哪家强?

区块链,比特币背后的分布式账本技术,日后可能会被证明比它支持的货币更有价值,但这种价值必须建立在安全的基础上。当我们开始将该技术付诸实践时,一定要确保我们设置的...

36060

简单介绍区块链技术

重要提示:如果你拥有价值超过1000美元的加密货币,那么你肯定应该使用硬件钱包,而不是在交易所保留硬币。 我推荐使用Trezor,你可以直接从他们的网站花费€8...

1.8K40
来自专栏开源项目

区块链技术到底是什么?| 码云周刊第 67 期

18450
来自专栏区块链大本营

3个基于Java的Blockchain库

30560
来自专栏深入浅出区块链技术

深入浅出区块链Wiki站开通

18160
来自专栏智能计算时代

区块链101:Ethereum(以太坊)是如何工作的?

现在,我们已经讨论了ethereum的内容,让我们深入了解一下这个平台在底层的功能。 考虑一下在线笔记本应用程序在“什么是Ethereum?” 使用ethere...

41150
来自专栏企鹅号快讯

投资钛值的你,知道钛链是什么吗?

技术 钛链技术到底有多牛?钛值从技术考量到底有多值?为普及和推广钛值背后的区块链技术,我们的钛值极客团队来给大家上课啦! 搬好小板凳,拿出笔记本,开始学习! 1...

27780

所以你是因此而想使用区块链么?

使用区块链有很好的理由和不好的理由。在与人们讨论区块链使用案例的对话中,我注意到一些普遍的困惑,以及源于最初狭义范围内的词语的合成词(通常用于描述比特币的区块链...

34880

扫码关注云+社区

领取腾讯云代金券