编程作业2: 在本作业中,您将使用 Solidity 和 ethers.js 在以太坊上实现一个复杂的去中心化应用程序即 DApp。您将编写一个智能合约和一个访问它的客户端程序, 从而了解 DApp 的“全栈”开发。为了节省您的时间,请在开始开发之前阅读整个作业——尤其是注意部分。 1 区块链版Splitwise 我们想要创建一个去中心化的系统来追踪AA制的开销——即做成 Splitwise 的区块链版本。如果您还没有听说过这个应用程序,可以简单了解一下,这是记录一群人在分摊午餐、百货或账单时,记录谁欠谁钱的简单方法。为了说明应用程序,考虑以下场景: Alice、Bob 和 Carol 都是喜欢一起出去吃饭的朋友。上次 Bob和 Alice 出去吃饭的时候,最后是Bob付了午餐费,所以Alice欠Bob 10 美元。同样,Carol和Bob出去吃饭时由Carol买单,所以鲍勃欠卡罗尔 10 美元。 现在,想象一下 Carol 现金短缺,向 Alice 借了 10 美元。请注意,此时,与其让每个人都在某个时候偿还他们的“欠款”,还不如大家一起先清理一下欠与被欠。换句话说,如果有债务循环,我们就可以把它从账本去掉,使得一切变得更简单,减少现金换手的次数。 我们将建立一种去中心化的方式来跟踪谁欠谁多少,这样就没有必要依赖可信的第三方。这将是高效的:它不会花费过多的gas来存储 这些数据。使用此应用程序不会在“区块链”上转移任何财富;唯一涉及的是用于 gas 的那些费用。 因为它在区块链上,当 Carol 为她和 Bob 的餐费填写支票时,她可以要求 Bob 提交一个借据(IOU, 他可以使用我们的 DApp 来完成),她只要确认他确实这么做了就行。存储在公有链上的信息,将作为谁欠谁的唯一事实来源。之后,当前面提到的债务循环得到解决时,Carol 将看到 Bob 不再欠她的钱。 作为程序的一部分,我们还将构建一个用户界面,为用户计算有用的信息并允许非程序员使用这个 DApp。 2 入门 1. 安装必备软件:您需要从 https://nodejs.org/en/ 下载并安装 Node.js。选择 LTS 版本(左侧)。 2. 运行 npm install -g ganache-cli 安装 Ganache CLI,我们将用它来模拟我们本地机器上的一个真实的以太坊节点。然后,运行 ganache-cli 来运行节点。你可以随时使用 Ctrl-C 停止节点。 3. 从课程网站下载并提取启动代码。 4. 在您的网络浏览器中打开 https://remix.ethereum.org。在“部署并运行交易(Deploy & run transactions)”选项卡(页面左侧)中,将环境设置为“Web3 Provider”,出现提示时单击“确定”,然后将“Web3 Provider Endpoint”设置为 http://localhost:8545 - 默认值是这个。在这里您将开发您的智能合约(您将在 Solidity 中写入)。 5. 在您的网络浏览器中打开 index.html 文件(您将看到一个标题为“Blockchain Splitwise’的页面)。打开浏览器的JavaScript 控制台,以便可以查看错误消息(有关如何执行此操作的链接位于本文档结尾)。如果到目前为止一切正常,您应该不会在控制台中看到任何错误 (您可能会看到警告;这没关系,有关更多详细信息,请参阅末尾的注释)。 6. 在你喜欢的 IDE 或文本编辑器(比如 SublimeText、Atom 或 Visual Studio Code 效果很好)打开代码目录。您将修改 script.js 以构建客户端,但查看其他文件可能会有所帮助。有些地方标有功能修改 - 请不要修改任何其他代码。您可以随意添加辅助函数。 7. 仔细阅读入门代码、web3.js API 和 Solidity 文档。在编写代码之前充分了解系统的整体设计。哪些数据应该存储在链上?哪些计算应该放在合约或客户端进行计算? 8. 实现满足如下要求的代码。写完您的合约后,部署它,然后在 script.js 中更新合约哈希和 ABI。 ABI 可以从“Solidity Compiler”选项卡到剪贴板,合约hash可以从“部署和运行交易”选项卡中的“已部署合约”面板中复制。请注意,合约哈希不是创建合约的交易的交易哈希。 操作系统备注:以上所有步骤都应适用于基于 Unix 的系统和 Windows。我们要求您执行的命令在标准 Unix 终端和 Windows 命令中都能运行。 3 要求 该项目有两个主要组成部分:一个智能合约,用 Solidity 编写并运行在区块链上,还有一个运行在本地浏览器的客户端,使用web3.js 获取区块链信息,并可以调用智能合约中的函数。 3.1 客户端功能 请注意,我们要求您实现的所有客户端功能都已在起始代码中以async函数的形式说明。这意味着它们默认返回一个promise。我们的评分系统假设从每个客户端函数返回promise。有关async函数、promise和await的知识,请参阅本讲义底部的参考。 1. getUsers():返回一个地址列表的 Promise。你可以返回:“曾经发送或收到借据的人”或“目前欠债或被欠债的人”。您可能发现把这个函数写成helper,对其他函数很有用。 2. getTotalOwed(user):返回给定用户所欠总金额的promise。 3. getLastActive(user):返回该用户最后记录的活动(发送借据或被列为借据上的“债权人”)的promise,UNIX 时间戳格式(自 1970 年 1 月 1 日以来的秒数)。如果没有活动,则返回 null。 4. send_IOU(creditor, amount):向合约提交借据,参数为债权人和金额。请参阅下面有关解决循环债务的说明。 3.2 合约中的功能 1. lookup(address debtor, address creditor) public view return(uint32 ret): 返回债务人debtor欠债权人creditor的金额。 2. add_IOU(address creditor, uint32 amount, ...):通知合约, msg.sender欠债权人(creditor)金额为amount。债务是叠加的:如果你已经欠钱了,则这个数值将增加。amount必须为正数。你可以让这个函数接受任何数量的附加参数。请参阅下面有关解决循环债务的说明。 欢迎您为客户或合约编写更多帮助程序。客户端可以使用 BlockchainSplitwise.methods.functionname(arguments).send() 调用合约函数。请参阅https://web3js.readthedocs.io/en/v1.2.9/web3-eth-contract.html#methods-mymethod-send 的文档,了解如何使用 send 和 call 从web3.js 的客户端调用 Solidity 合约函数 。请记住,客户端函数用 JavaScript 编写,合约函数用 Solidity 编写。 4 解决债务循环 将借据视为债务图是有帮助的。也就是说,假设每个用户都是一个节点,并且每个从 A 到 B 且权重为 X 的加权有向边表示“A 欠 B 计X美元”这一事实。我们将把它写成 A-(X)→ B. 我们希望应用程序通过循环减去中每个步骤中的所有权重的最小值来消除债务循环(也就是使每一个循环中至少有一个步骤权重为“0”)。 例如,如果 A-15→ B 和 B-11→ C, 当 C 去加 C-16→ A时,实际余额为 A-4→ B, B-0→ C, 和 C-5-→ A. 类似地,如果现在增加 C-9→ A,则更新后的实际余额是 A-6-→ B, B-2→ C, 和 C-0-→ A. 要求是,如果在使用客户端 (add_IOU)添加 IOU时,可能形成任何潜在的循环,您必须至少“解析”其中之一。你不必担心关于涉及多个循环的复杂情况,或优化要采用的路径(类似于 max流).在这些情况下。您可以假设这是两个合约函数 (add_IOU和loop)的前提条件,图中不存在循环。最后,您还可以假设找到的任何循环都比较小(比如,小于 10)。 我们在代码中为您提供了广度优先搜索算法 - 要使用它,请传入一个开始和结束节点,以及获取任何给定节点的“邻居”的函数。你可以选择不使用这个函数。 这个保证解决方案的安全性取决于您。不应该能使恶意的客户在发布借条后后再以某种方式“抹去”他们的债务。 我们现在可以准确地说明如何在这个系统中偿还欠条。说爱丽丝借了鲍勃 10 美元;现在,她想用现金偿还鲍勃。当爱丽丝给鲍勃 10 美元现金时,鲍勃将添加 10 美元的借据,债权人为 Alice。这将创建一个循环:具体来说,A-10→ B,和B-10→ A. 根据上面的循环解决方案,最终是两清 A-0→ B 和 B-0-→ A. 5 总体要求 欢迎您以任何您喜欢的方式编写您的合约,只要它具有指定的lookup和 add_IOU 函数。您的目标是编写一份最小化存储量和两个合约函数计算开销的合约。这将最大限度地减少gas成本。 您可以假设交易量足够小,小到搜索客户端可以搜索整个区块链,但您不应该假设所有用户就是您的用户钱包中的那些用户 - 换句话说, provider.listAccounts() 不包含系统中所有可能的用户。 6 提交代码 我们将使用 Gradescope 进行提交。您提交的内容将被评分器依照是否正确地回答了查询以及是否产生了合理的 gas 量来判断。提交前, 请确保提交mycontract.sol和script.js。 7 备注 7.1 系统架构 -您应该首先决定将哪些数据结构存储在区块链上。仔细考虑了解您需要提供给客户的信息。你不需要使用任何特别花哨的数据结构。您的决定可能会使实现变得很难,此时你应该可以回过头去改变你的架构。 -我们没有提到在两个人之间形成循环的情况下该怎么做。我们建议这样设计您的系统,以使得这种情况成为一个特例 - 当债务人“偿还”债权人时,债权人只是试图在相反的方向添加借据,这将触发循环解析并以彼此欠 0 结束。我们也推荐避免任何“负”债务的概念,因为这会使事情变得过于复杂。 -请记住,在优化 gas 成本时,客户端上运行的函数是免费的——它们不产生成本。 -我们建议您在设计系统后,开始在Remix 中编写和充分调试合约。可以调用右下面板的函数,使用右上角的“帐户”选择器切换账号。要将地址复制到剪贴板,您可以单击选择器旁边的复制图标。一旦你确定合约按预期工作,那么你应该开始编写客户端。 -您不需要大量代码来完成任务。我们的解决方案是大概40 行 Solidity 和大约 70 行新的 JavaScript(不包括 ABI)。 7.2 实际开发调试 - 要调试客户端代码,请放心使用console.log。你可以看到函数调用结果,以及它们来自浏览器的 JavaScript 控制台的行号。 - 关于同步 XMLHttpRequest 的警告可以忽略。 - Solidity 有一个非常有用的函数 require 可以让你检查依赖 - 如果您只想调试您的合约,您可以使用“JavaScript VM”Web3 Provider ,然后在失败的事务上按“调试”。确保之后切换回来,以便 您可以运行客户端代码。 - autograder 将假设客户端函数返回 Promises 以获得其他值。要了解有关 promise 和异步代码的更多信息,请参阅本讲义的底部参考资料 。我们还提供了一个健全性检查测试功能,应该使您能够了解我们将如何测试您的代码。鼓励你写其他测试,但请确保您的代码在提交之前通过了提供的完整性检查。 - ABI 解码器将解码所有小写的函数输入。为了适应这种情况,入门代码尝试使用toLowerCase()函数将所有值转为小写版本后返回。请记住直接保持大写和小写值。当字母作为十六进制值的一部分时,不管字母字符的大小写如何,每个函数都应该正常工作。 - 请注意,我们使用的是 Solidity 版本 0.8.17,因此不要使用旧版本的文档,因为它们的行为可能不同。 8 参考 您可以在此处阅读有关如何打开浏览器的 JavaScript 控制台的信息。(按F12就可以了) ? 此处为 Javascript 中的async/await指南。 ? 此处为 Javascript 中的 Promise 指南。 Hardhat文档 ethers.js 文档 Solidity v0.8.17文档