为想创建项目的朋友搭建创业平台,请感兴趣的朋友加乐乐微信:sensus113
NervosFans 微信公号:Nervosfans
谢谢!
背景
几个月前,我和我同事Samm觉得是时候学习下以太坊、Solidity的开发了。我们对以太坊自推出以来的各项进展都属于被动关注,所以决定以业余时间开发个以太坊Dapp的方式了解平台的底层技术。
对于要构建的Dapp,我们有目标若干:
1. 这个Dapp的体量不会很太大,但是能涵盖以太坊、Solidity的所有有意义的功能。
2. 这个Dapp不用我们提供任何云服务也能运行,就是说我们只负责编写客户端代码与以太坊智能合约。
最后,我们开发出的Dapp是一款叫做Eth Plot的小游戏,灵感来自Reddit上著名的r/place愚人节玩笑和million dollar homepage。 玩家可以购买网格上一块一块(plots)的数字空间(大概其数字地块的意思)。购置后可在地块上放置自己喜欢的图像以及网站链接,还可以随时转卖。
感兴趣的盆友可以查看这里!Dapp运行在(以太坊)主网还有Ropsten、Rinkeby和Kovan上。我们希望有人会用这个Dapp,但是对与我们来说项目的教育意义才是主要的,那么让我们借助这篇文章分享一下我们开发这个小项目的一些心得。
注:文章略长,想直接看代码的点这里:智能合约、React/Redux应用。 希望这些示例代码能对那些正着手构建自己首个Dapp的人有所帮助。
本文的目标受众是那些熟悉以太坊但由于尚未编写过Dapp,因此需要了解下大概开发过程的开发者,文章还附上了我们在开发过程中get的一些小技巧。我们不自诩是专家,也不准备再撰写其他的Dapp开发教程。
以下是我们将在本文中介绍的内容:
• 技术堆栈的高级概述,即构建Eth Plot使用的各项技术。
• Eth Plot智能合约详情,以高效方式实现此应用程序所需的功能着实复杂。
• 小技能,就是小贴士。
技术堆栈
本项目中,我们使用了一下堆栈:
• 前端开发使用React/Redux/Material UI/TypeChain
• 生产的Web3提供程序使用MetaMask
1. 智能合约
Eth Plot的核心功能(以去中心化方式买卖地块的能力)只能通过以太坊智能合约功能实现。 我们也经常使用了事件,实现了低成本存储的同时还支持了Web应用的响应能力。 由于EthPlot背后的主智能合约才是这个项目中最为有趣的方面,我们花一整章的时间专门讨论。
2. IPFS
我们知道,链上存储图像数据相当费钱。 所以我们采取了当前的标准操作,先把图像上传到IPFS,拿到图像文件的哈希,再把这个哈希存储到链上。尽管数据并未全部存储到以太坊,Eth Plot仍保持了纯粹的去中心化属性。为避开自己运行IPFS节点的各种琐事,我们使用了Infura的IPFS服务。
3. Truffle/Ganache
Truffle是一组能让以太坊合约测试变得更轻松的有用工具。我们智能合约从编译到本地以太坊节点部署都使用了Truffle。 Ganache是我们的本地以太坊节点,确切的说是个JavaScript轻量级以太坊客户端。Truffle还被用作测试框架测试我们的智能合约。
4. React/Redux/MaterialUI/TypeChain
前端我们选择了React,并使用Redux进行状态管理。 有鉴于这种操作在开发中非常普遍,这里就不多说了。详细信息可以查看源代码。
我们还使用了Material UI项目中的组件。 Material UI属于一组实现Google Material Design规范的React组件。 这个库写得非常好,文件很不错,里边的组件也很不错。
我们习惯用TypeScript编写基于Web的项目,因此使用了一个叫TypeChain的项目,能为Solidity合约提供TypeScript绑定。
5. MetaMask
用户与Eth Plot交互需要安装MetaMask。MetaMask的使用非常简单,但仍有些例外:
1) 由于缓存量无法预测,MetaMask并不适合本地测试。 虽说有“重置帐户”的功能,也还是会出现交易状态被缓存的情形,进而阻碍开发。 有鉴于此,我们在本地开发时直接使用了Ganache的web3提供程序。
2) MetaMask状态处理用户帐户更改的能力不理想。 建议的方法是以一定时间间隔重复检查当前帐户。
话虽如此,MetaMask仍是一个很棒的项目,Dapp空间缺了它可不成。
智能合约DeepDive
支持Eth Plot的主智能合约应该是本项目最具挑战性、最独特的方面(之一),主要因为在合约中存储数据并执行大量计算成本巨大。
首先,我们列出了合约的一些必要条件:
1) 定义一个约束尺寸至少为250x250的网格系统。
2) 网格中的每个坐标代表一个1x1的地块(plot),包含一名所有者、一个收购价、一个网站以及与之相关的可视数据。
3) 创建带有图像的连续区域时,地块面积可以大于1x1。
4) 为支持用户购置自己中意的任意地块(意思是用户不用非得买一块整地),地块可以拆分出售,且一个地块可以与另一个地块重叠。
1. 原始方法
原始的解决方法是创建一个带大型2D数组的合约。 数组中的每个条目代表完整网格中的一个坐标,且包含该坐标的相应信息。 购置地块时,买方发送交易,并在交易中表明意向购置地块的坐标以及要关联的数据。
不幸的是,考虑到存储数据的gas价格(就是SSTORE操作),譬如存储一个32字节的单词要2万个gas,我们的数组中需要存储6.25万(250x250)个单词,也就是要花掉12.5亿个gas。Gas价格按5 gwei算,那么交易成本约6.25ETH(撰写本文时6.25 ETH≈3750美元!)。 况且,这个gas消耗还是单个区块gas上限(撰写时为800万)的300倍,超的太离谱。 即便我们成功部署了这种合约,交互成本这么高也不会有人用的。
2. 我们的方法:合约存储
最终,我们想出了一个能够有效利用有限资源的办法。
来看看我们是如何存储合约状态的。
首先,我们存储的是地块的摘要(summaries),即地块的起始坐标(x,y)和尺寸(宽、高),而非代表单个坐标的2D数组。这样做的好处是存储成本不会随地块面积线性增长,也就是说大地块的存储成本跟小地块是相当的。合约按购买顺序存储地块数组,后购买的地块排在先购买的地块之后,这就是合约中的所有权数组。所有权数组内有每个地块的登记(信息),包括其几何结构(x,y,w,h)和所有者地址。由于几何机构的范围在0-250之间,我们可以把这四个值都存储在一个3字节的uint24变量中。加上所有者地址的20字节,数组中每个登记长度为20 +(4*3)= 32个字节。这也是EVM中的字大小,因此存储更高效。
有人可能疑惑这种数据表示法如何支持部分地块的出售?这里,我们借助了孔映射(holes mapping)来跟踪哪些地块彼此重叠,这些重叠说明有后来的地块购置了之前地块的某个部分(subsection)。以下是所有权与孔数组之间关系的示例表示。
添加此重叠逻辑相当有必要,原因我们需要能够快速验证某人试图购买的地块是否有效。保持孔映射始终处于最新的状态,也就是意味着验证新购置时内存中需要保存的状态更少,进而防止出现原始方法中超gas上限300倍的情形出现。
我们也没有把所有图像数据直接存储在区块链上,而是把特定地块的图像上传至IPFS,然后把图像的IPFS哈希存储在合约中。除了图像哈希,我们也把与地块相关的网站也存进了智能合约。这个信息有自己的映射,与所有权对象分开存储,因此计算新购置时不用将其读入内存,所以购买交易的成本会更低一些。该信息存储在数据映射中。
最后,我们把特定地块当前的收购价存储在一个名为plotIdToPrice的单独映射中。拥有者用户可随时更新地块收购价。同数据映射,这个信息也与所有权数据分开存储,因为它被访问的频率并不高(仅在计算支出时使用)。
3. 我们的方法:地块购置
看过数据存储,接下来我们了解下地块购置的过程。购买新地块时,调用名为purchaseAreaWithData的函数。数据被规定成某种独特格式,这么做是为了让合约的执行尽可能高效。
我们的初始版本中,调用者仅传入自己要购买的矩形,由合约计算需要购买的所有子地块。这种方法在小用例中没问题,但是后来我们很快就推到了EVM的上限,并且遇到了交易成本膨胀以及加载到内存中的数据量导致的stack-too-deep错误。于是我们换成了调用者提前进行所有计算,合约仅验证计算结果并转移资金这种方式。调用者先发送一系列子地块,这些子地块构成了意象购买区域的完整平铺。这些子地块也代表了现有地块中即将被购置的各部分。除了子地块之外,子地块所在地的索引也被传入函数。
合约本身吸引了大量评论,但是值得一探究竟,以下是对购置函数作用的高级概述
• 输入参数的验证与边界检查
• 检查传入的子地块是否形成了购置地块的完整平铺
• 检查确保所有购置子地块处于待售状态,且仍属于调用人意向从此(地块)购入的母地块(这里用到了孔结构)。
• 使用交易中发送的资金支付所有子地块所有者,并为迁移发出事件。
• 存储新地块、新数据、收购价,使用新的购置更新所有孔数组,发出已购置事件。
4. 其他功能
合约剩余部分还有点其他的琐碎函数,譬如所有者可访问的更改地块收购价与地块数据的函数以及将内容标记为非法并对合约中收集资金进行撤回的管理函数。 然后,还有些视图函数,通过聚合各独立数据结构的信息实现合约数据更高效的读取。
5. 为何不用ERC-721?
我们在设计时知道有ERC-721,也觉得不错,可惜与我们的要求不符。 使用ERC-721最大的问题是无法对地块进行细分,因此不满足可出售部分地块这个要求。ERC-721的另一个问题是token一旦被细分就没办法重新组合成更大的地块。
小贴士
下面是小贴士分享时间。
确定交易成本很难,不妨先写一些测试,猜一下,然后检查:编写单元测试算是非常有用优化技巧了,这些测试贯穿(我们认为)一组合约的代表性交易。计算出测试的gas成本后,可以尝试重构合约,查看(重构)对gas消耗的影响。通过这种方式,我们把合约效率提高了40%。
链下计算,链上验证:能不在合约中进行的计算尽量别放在合约中。发现代码越写越复杂的时候,回看下能否把计算尽量移至链下,合约验证计算结果即可。譬如,我们把大部分的购置计算都放在了客户端 (示例代码)中。
留意存储的结构,映射可能比数组更好:各种优化工作中一个有趣的发现是,把相关数据拆分成多个对象能够产生深远影响。由于EVM必须一次将全字加载到内存中,因此仅将地块几何与所有者存储在所有权数组中可以防止加载图像、收购价等等那些没必要一起载入的数据。此外,对于其他数据结构,使用映射会比并行数组便宜不少,原因是不需要更新数组的长度值(更新涉及SSTORE这波贵操作)。
Truffle调试器心情好的时候表现也巨好:Truffle框架支持部署本地测试网、编写单元测试以及合约调试,用处很大。用Truffle调试器(truffle debug指令)用调试手动测试期间失败的交易也非常有用。不幸的是,由于Truffle单元测试的设置方式,调试器不支持对源自单元测试的交易进行调试。
Truffle默认不启用优化:撰写本文时,我们意识到Truffle编译默认下不启用合约优化。 但是启用后可以节省约20%的gas。可以通过truffle-config文件启用。
结论
感谢阅读!
这个项目让我们受益良多,我们还计划在未来做更多基于以太坊的开发。
热烈欢迎感兴趣,特别是有开发意向的的盆友查看git上的代码、留下你的意见或建议。
Update: 2018/08
主网上线后还有新发现!近期体验过游戏的用户可能察觉到开始时的加载速度巨慢。 这是因为为了节省成本,我们把app数据存储在了事件日志当中,缺点就是搜索日志极度耗时,特别在主网上。 由于大小的原因,我们在本地测试、测试网上没有注意到这个缺陷。
为解决这个问题,需要添加一个专门的缓存层。目测我们应该是没什么时间做这事了,因为有其他项目在开发(拿个小本本记下来好了)。未来的话, Infura(或Infura的竞争对手!)应该会提供对此问题的相关功能。
Update: 2018/11
EthPlot是一针鸡血,之后我们启动了Nodesmith.io项目,希望能为开发者提供构建去中心化应用所需的一切服务,而且这个服务的可用性不会被底层区块链基础设施并所限制。Nodesmith中,我们提供的API不仅能让开发者访问底层区块链基础设施(譬如发送交易、读取网络状态),还能为应用提供使其更加用户友好的各种服务。譬如,为了实现时间日志的快速读取,我们正在添加智能合约事件缓存。 我们还添加了一个webhooks API,因此开发者不必时常轮询网络。
https://medium.com/coinmonks/what-we-learned-building-our-first-dapp-28b01f9fc244
网友评论