diff --git a/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract/index.md b/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract/index.md new file mode 100644 index 00000000000..d1038b69f1d --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract/index.md @@ -0,0 +1,359 @@ +--- +title: "給初學者的 Hello World 智能合約" +description: "在以太坊上撰寫和部署簡單智能合約的入門教學。" +author: "elanh" +tags: [ "solidity", "hardhat", "alchemy", "smart contracts", "deploying" ] +skill: beginner +lang: zh-tw +published: 2021-03-31 +--- + +如果你是區塊鏈開發新手,不知道從何開始,或者你只是想了解如何部署智能合約並與之互動,本指南就是為你準備的。 我們將使用虛擬錢包 [MetaMask](https://metamask.io/)、[Solidity](https://docs.soliditylang.org/en/v0.8.0/)、[Hardhat](https://hardhat.org/) 和 [Alchemy](https://www.alchemy.com/eth) 逐步說明如何在 Sepolia 測試網上建立和部署一個簡單的智能合約 (如果你還不了解這些術語的含義,別擔心,我們會解釋的)。 + +在本教學的[第 2 部分](https://docs.alchemy.com/docs/interacting-with-a-smart-contract)中,我們將介紹部署後如何與我們的智能合約互動,並在[第 3 部分](https://www.alchemy.com/docs/submitting-your-smart-contract-to-etherscan)中介紹如何在 Etherscan 上發布它。 + +如果你有任何問題,隨時可以在 [Alchemy Discord](https://discord.gg/gWuC7zB) 中提問! + +## 第 1 步:連接到以太坊網路 {#step-1} + +向以太坊鏈發出請求有很多種方式。 為求簡單,我們將使用 Alchemy 的免費帳戶,它是一個區塊鏈開發者平台暨 API,讓我們不用運行自己的節點,就能與以太坊鏈通訊。 該平台還提供用於監控和分析的開發者工具,我們將在本教學中利用這些工具來了解我們智能合約部署的底層情況。 如果你還沒有 Alchemy 帳戶,可以[在這裡免費註冊](https://dashboard.alchemy.com/signup)。 + +## 第 2 步:創建你的應用程式 (和 API 金鑰) {#step-2} + +一旦你已經創建好一個Alchemy的帳戶,你可以通過建立一個程式來生成一個API鑰匙。 這將讓我們能向 Sepolia 測試網發出請求。 如果你不熟悉測試網,請查看[此頁面](/developers/docs/networks/)。 + +1. 在你的 Alchemy 儀表板中,導覽至「Create new app」頁面,方法是在導覽列中選擇「Select an app」,然後點擊「Create new app」。 + +![Hello world 創建應用程式](./hello-world-create-app.png) + +2. 將你的應用程式命名為「Hello World」,提供簡短描述,並選擇一個使用案例,例如「Infra & Tooling」。 接著,搜尋「Ethereum」並選擇網路。 + +![創建應用程式檢視 hello world](./create-app-view-hello-world.png) + +3. 點擊「Next」繼續,然後點擊「Create app」,就完成了! 你的應用程式應該會出現在導覽列的下拉式選單中,並提供可供複製的 API 金鑰。 + +## 第 3 步:建立一個以太坊帳戶 (地址) {#step-3} + +我們需要一個乙太坊帳戶去接收或發送交易。 為此教學,我們將會使用 MetaMask。它是一個在瀏覽器上管理你的乙太坊帳戶地址的虛擬錢包。 更多關於[交易](/developers/docs/transactions/)的資訊。 + +你可以[在這裡](https://metamask.io/download)免費下載 MetaMask 並建立一個以太坊帳戶。 當你在建立帳戶時,或者如果你已經有帳戶,請務必使用網路下拉式選單切換到「Sepolia」測試網 (這樣我們就不用處理真實貨幣)。 + +如果你沒有看到 Sepolia 列出,請進入選單,然後到「Advanced」,向下捲動以開啟「Show test networks」。 在網路選擇選單中,選擇「Custom」分頁以尋找測試網列表,然後選擇「Sepolia」。 + +![metamask sepolia 範例](./metamask-sepolia-example.png) + +## 第 4 步:從水龍頭獲取以太幣 {#step-4} + +為了將我們的智能合約部署到測試網,我們需要一些假的 Eth。 要取得 Sepolia ETH,你可以前往 [Sepolia 網路詳細資料](/developers/docs/networks/#sepolia)來查看各種水龍頭的列表。 如果其中一個不能用,試試另一個,因為它們有時可能會用完。 由於網路流量的關係,可能需要一些時間才能收到你的假 ETH。 不久之後,你應該會在你的 Metamask 帳戶中看到 ETH! + +## 第 5 步:檢查你的餘額 {#step-5} + +為再次確認我們的餘額,讓我們使用 [Alchemy 的 composer tool](https://sandbox.alchemy.com/?network=ETH_SEPOLIA&method=eth_getBalance&body.id=1&body.jsonrpc=2.0&body.method=eth_getBalance&body.params%5B0%5D=&body.params%5B1%5D=latest) 發出一個 [eth_getBalance](/developers/docs/apis/json-rpc/#eth_getbalance) 請求。 這將會回傳你的錢包裡的餘額。 在你輸入自己的MetaMask帳戶地址,並且點下「寄送請求」後,你理應會看見一個這樣子的回應: + +```json +{ "jsonrpc": "2.0", "id": 0, "result": "0x2B5E3AF16B1880000" } +``` + +> **注意:**此結果以 wei 為單位,而非 ETH。 Wei是一個被用來計算以太最少分數的單位。 wei 與 ETH 的換算為:1 eth = 1018 wei。 因此,如果我們將 0x2B5E3AF16B1880000 轉換為十進位,我們會得到 5\*10¹⁸,相當於 5 ETH。 +> +> 哈! 我們的假錢都在這裡了 。 + +## 第 6 步:初始化我們的專案 {#step-6} + +首先,我們需要一個資料夾給我們的專案。 前往到你的指令介面(powershell, cmd 或 Terminal) 接著輸入: + +``` +mkdir hello-world +cd hello-world +``` + +現在我們在專案資料夾中了,我們將使用 `npm init` 來初始化專案。 如果你還沒有安裝 npm,請遵循[這些說明](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm) (我們也需要 Node.js,所以也請下載它!)。 + +``` +npm init +``` + +你如何回答安裝問題並不重要,以下是我們的做法,僅供參考: + +``` +套件名稱:(hello-world) +版本:(1.0.0) +描述:hello world 智能合約 +進入點:(index.js) +測試指令: +git 儲存庫: +關鍵字: +作者: +授權:(ISC) +即將寫入 /Users/.../.../.../hello-world/package.json: + +{ + "name": "hello-world", + "version": "1.0.0", + "description": "hello world 智能合約", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} +``` + +核准 package.json,我們就可以開始了! + +## 第 7 步:下載 [Hardhat](https://hardhat.org/getting-started/#overview) {#step-7} + +Hardhat 是一個開發環境,提供你去編譯、部屬、測試、以及除錯你的以太坊軟體。 它能協助開發人員在部署至即時鏈之前,於本機建立智慧合約和去中心化應用程式。 + +在我們的 `hello-world` 專案中執行: + +``` +npm install --save-dev hardhat +``` + +如需更多[安裝指示](https://hardhat.org/getting-started/#overview)的詳細資訊,請查看此頁面。 + +## 第 8 步:建立 Hardhat 專案 {#step-8} + +在你的專案資料夾下執行: + +``` +npx hardhat +``` + +你接下來會看到歡迎訊息以及關於你想做什麼的選項。 選擇"create an empty hardhat.config.js": + +``` +888 888 888 888 888 +888 888 888 888 888 +888 888 888 888 888 +8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 +888 888 "88b 888P" d88" 888 888 "88b "88b 888 +888 888 .d888888 888 888 888 888 888 .d888888 888 +888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. +888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + +👷 歡迎來到 Hardhat v2.0.11 👷‍ + +你想做什麼?… +建立範例專案 +❯ 建立一個空的 hardhat.config.js +退出 +``` + +這會為我們產生一個 `hardhat.config.js` 檔案,我們將在其中指定專案的所有設定 (在第 13 步)。 + +## 第 9 步:新增專案資料夾 {#step-9} + +為了讓我們的專案井然有序,我們將建立兩個新資料夾。 在你的指令介面返回到到專案資料夾,接著輸入: + +``` +mkdir contracts +mkdir scripts +``` + +- `contracts/` 是我們存放 hello world 智能合約程式碼檔案的地方 +- `scripts/` 是我們存放部署和與合約互動的腳本的地方 + +## 第 10 步:撰寫我們的合約 {#step-10} + +你可能會問自己,我們到底什麼時候才要寫程式碼? 嗯,我們現在就在第 10 步。 + +在你喜歡的編輯器中打開 hello-world 專案 (我們喜歡 [VSCode](https://code.visualstudio.com/))。 智能合約是以一種稱為 Solidity 的語言編寫的,我們將用它來編寫我們的 HelloWorld.sol 智能合約。‌ + +1. 導覽至「contracts」資料夾並建立一個名為 HelloWorld.sol 的新檔案。 +2. 以下是來自以太坊基金會的 Hello World 智能合約範例,我們將在本教學中使用它。 複製並貼上下方內容到你的 HelloWorld.sol 檔案中,並務必閱讀註解以了解此合約的功能: + +```solidity +// 指定 Solidity 的版本,使用語意化版本。 +// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity ^0.7.0; + +// 定義一個名為 `HelloWorld` 的合約。 +// 合約是函式和資料 (其狀態) 的集合。一旦部署,合約就會位於以太坊區塊鏈上的特定地址。了解更多:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + // 宣告一個 `string` 型別的狀態變數 `message`。 + // 狀態變數是其值永久儲存在合約儲存空間中的變數。關鍵字 `public` 使變數可從合約外部存取,並建立一個其他合約或用戶端可以呼叫以存取該值的函式。 + string public message; + + // 與許多以類別為基礎的物件導向語言類似,建構函式是一個特殊函式,只在合約建立時執行。 + // 建構函式用於初始化合約的資料。了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 接受一個字串引數 `initMessage`,並將其值設定到合約的 `message` 儲存變數中。 + message = initMessage; + } + + // 一個公開函式,接受一個字串引數並更新 `message` 儲存變數。 + function update(string memory newMessage) public { + message = newMessage; + } +} +``` + +這是一個非常簡單的智能合約,它在建立時儲存一則訊息,並可以透過呼叫 `update` 函式進行更新。 + +## 第 11 步:將 MetaMask 和 Alchemy 連接到你的專案 {#step-11} + +我們已經建立了 MetaMask 錢包、Alchemy 帳戶,並編寫了我們的智能合約,現在是時候將這三者連接起來了。 + +每一個從你的虛擬錢包送出的交易都需要用你的私鑰簽名。 為了給予程式這個權限,我們可以把私鑰(還有 Alchemy API key)存在環境檔案中。 + +> 若要深入了解傳送交易,請參閱這篇關於使用 web3 傳送交易的[教學文章](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)。 + +首先,安裝 dotenv 套件。 + +``` +npm install dotenv --save +``` + +然後,在我們專案的根目錄中建立一個 `.env` 檔案,並在其中新增您的 MetaMask 私鑰和 HTTP Alchemy API URL。 + +- 遵循[這些說明](https://support.metamask.io/configure/accounts/how-to-export-an-accounts-private-key/)匯出你的私鑰 +- 請參閱下文以取得 HTTP Alchemy API URL + +![取得 alchemy api 金鑰](./get-alchemy-api-key.png) + +複製 Alchemy API URL + +你的 `.env` 應該看起來像這樣: + +``` +API_URL = "https://eth-sepolia.g.alchemy.com/v2/你的-api-金鑰" +PRIVATE_KEY = "你的-metamask-私鑰" +``` + +為了實際將這些連接到我們的程式碼,我們將在第 13 步的 `hardhat.config.js` 檔案中引用這些變數。 + + + + +不要提交 .env! 請務必不要與任何人分享或洩露您的 .env 檔案,因為這樣做會洩露您的機密。 如果您正在使用版本控制,請將您的 .env 新增到 gitignore 檔案中。 + + + + +## 第 12 步:安裝 Ethers.js {#step-12-install-ethersjs} + +Ethers.js 是一個函式庫,它將[標準 JSON-RPC 方法](/developers/docs/apis/json-rpc/)包裝成更方便使用者使用的方法,讓與以太坊互動和發出請求變得更簡單。 + +Hardhat 讓整合[外掛程式](https://hardhat.org/plugins/)以取得額外工具和擴充功能變得超級簡單。 我們將利用 [Ethers plugin](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers) 進行合約部署 ([Ethers.js](https://github.com/ethers-io/ethers.js/) 有一些非常簡潔的合約部署方法)。 + +在你的專案目錄輸入: + +``` +npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0" +``` + +我們也將在下一步的 `hardhat.config.js` 中引入 ethers。 + +## 第 13 步:更新 hardhat.config.js {#step-13-update-hardhatconfigjs} + +我們目前已經新增了幾個相依套件和外掛程式,現在我們需要更新 `hardhat.config.js`,讓我們的專案知道它們全部。 + +將你的 `hardhat.config.js` 更新成如下所示: + +``` +require('dotenv').config(); + +require("@nomiclabs/hardhat-ethers"); +const { API_URL, PRIVATE_KEY } = process.env; + +/** +* @type import('hardhat/config').HardhatUserConfig +*/ +module.exports = { + solidity: "0.7.3", + defaultNetwork: "sepolia", + networks: { + hardhat: {}, + sepolia: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`] + } + }, +} +``` + +## 第 14 步:編譯我們的合約 {#step-14-compile-our-contracts} + +為了確認一切運作正常,我們來編譯合約。 `compile` 任務是內建的 hardhat 任務之一。 + +在命令列工具輸入: + +``` +npx hardhat compile +``` + +你可能會收到關於 `SPDX license identifier not provided in source file` 的警告,但不用擔心——希望其他一切都看起來沒問題! 如果沒有,您隨時可以在 [Alchemy discord](https://discord.gg/u72VCg3) 中傳送訊息。 + +## 第 15 步:編寫我們的部署腳本 {#step-15-write-our-deploy-scripts} + +現在我們已經寫好了合約,並且也搞定配置檔案。現在我們該來撰寫部署合約的腳本。 + +導覽至 `scripts/` 資料夾並建立一個名為 `deploy.js` 的新檔案,將以下內容加入其中: + +``` +async function main() { + const HelloWorld = await ethers.getContractFactory("HelloWorld"); + + // 開始部署,回傳一個解析為合約物件的 promise + const hello_world = await HelloWorld.deploy("Hello World!"); + console.log("合約已部署至地址:", hello_world.address);} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); +``` + +Hardhat 在其[合約教學文章](https://hardhat.org/tutorial/testing-contracts.html#writing-tests)中詳細地解釋了每一行程式碼的作用,我們在此採用了他們的解釋。 + +``` +const HelloWorld = await ethers.getContractFactory("HelloWorld"); +``` + +ethers.js 中的 `ContractFactory` 是用於部署新智能合約的抽象,因此這裡的 `HelloWorld` 是我們 hello world 合約實例的工廠。 使用 `hardhat-ethers` 外掛程式時,`ContractFactory` 和 `Contract` 實例預設會連接到第一個簽署者。 + +``` +const hello_world = await HelloWorld.deploy(); +``` + +在 `ContractFactory` 上呼叫 `deploy()` 將開始部署,並回傳一個解析為 `Contract` 的 `Promise`。 這就是和我們的智慧型合約函數有一對一的方法的物件。 + +## 第 16 步:部署我們的合約 {#step-16-deploy-our-contract} + +我們終於準備好要部署合約了! 導覽至命令列並執行: + +``` +npx hardhat run scripts/deploy.js --network sepolia +``` + +你會看到像這樣的輸出: + +``` +合約已部署至地址:0x6cd7d44516a20882cEa2DE9f205bF401c0d23570 +``` + +如果我們前往 [Sepolia etherscan](https://sepolia.etherscan.io/) 並搜尋我們的合約地址,我們應該能夠看到它已成功部署。 這個交易執行看起來會像這樣: + +![etherscan 合約](./etherscan-contract.png) + +`From` 地址應與你的 MetaMask 帳戶地址相符,而 To 地址會顯示「Contract Creation」,但如果我們點進交易,我們會在 `To` 欄位中看到我們的合約地址: + +![etherscan 交易](./etherscan-transaction.png) + +恭喜! 你剛剛成功將一個智能合約部署到以太坊鏈了 🎉 + +為了了解幕後情況,讓我們前往 [Alchemy 儀表板](https://dashboard.alchemyapi.io/explorer)中的「Explorer」分頁。 如果你有多個 Alchemy 應用程式,請務必依應用程式篩選並選擇「Hello World」。 +![hello world 瀏覽器](./hello-world-explorer.png) + +在這裡你會看到一些 Hardhat/Ethers 在我們呼叫 `.deploy()` 函式時,在底層為我們發出的 JSON-RPC 呼叫。 這裡要特別提出兩個重要的呼叫,一個是 [`eth_sendRawTransaction`](https://www.alchemy.com/docs/node/abstract/abstract-api-endpoints/eth-send-raw-transaction),這是實際將我們的合約寫入 Sepolia 鏈的請求;另一個是 [`eth_getTransactionByHash`](https://www.alchemy.com/docs/node/abstract/abstract-api-endpoints/eth-get-transaction-by-hash),這是在給定哈希值的情況下讀取我們交易資訊的請求 (這是交易時的典型模式)。 要了解更多關於發送交易的資訊,請查看這篇關於[使用 Web3 發送交易](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)的教學。 + +本教學的第 1 部分到此結束,在第 2 部分中,我們將實際[與我們的智能合約互動](https://www.alchemy.com/docs/interacting-with-a-smart-contract),方法是更新我們的初始訊息;在第 3 部分中,我們將[把我們的智能合約發布到 Etherscan](https://www.alchemy.com/docs/submitting-your-smart-contract-to-etherscan),這樣每個人都會知道如何與它互動。 + +**想了解更多關於 Alchemy 的資訊嗎?** 請查看我們的[網站](https://www.alchemy.com/eth)。 不想錯過任何更新嗎? [在此](https://www.alchemy.com/newsletter)訂閱我們的電子報! 也請務必加入我們的 [Discord](https://discord.gg/u72VCg3)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-implement-an-erc721-market/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-implement-an-erc721-market/index.md new file mode 100644 index 00000000000..d27633470ea --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-implement-an-erc721-market/index.md @@ -0,0 +1,145 @@ +--- +title: "如何導入一ERC-721市場" +description: "如何販售代幣化物件於去中央化訊息版." +author: "Alberto Cuesta Cañada" +tags: [ "smart contracts", "erc-721", "solidity", "tokens" ] +skill: intermediate +lang: zh-tw +published: 2020-03-19 +source: Hackernoon +sourceUrl: https://hackernoon.com/how-to-implement-an-erc721-market-1e1a32j9 +--- + +於此文章, 我們將介紹如何為以太坊區塊鏈程式編輯Craigslist之物件訊息版. + +於Gumtree, Ebay 及 Craigslist, 分類訊息版通常由紙本或軟木所組成. 此為分類訊息版於學校走廊, 報紙, 跑馬燈, 及店面廣告. + +此全部因網路之導入而大幅改變. 能看見特殊分類訊息版的人數因網路而大幅提升. 與此, 市場代表將成為更加有效率且能夠擴張至全球範圍. Ebay為一龐大事業且其原始商業模式源於此實體分類訊息版模式. + +區塊鏈技術能使其市場再次改變. 讓我們來看看此如何發生. + +## 營利 {#monetization} + +一基於公共區塊鏈之商業模式的分類訊息版將與Ebay及其他公司看起來大大不同. + +首先,從[去中心化的角度](/developers/docs/web2-vs-web3/)來看。 既有平台需要來維持其擁有服務. 一去中央化平台是由其用戶所維持, 所以就平台持有者之角度來看, 運作平台核心之成本費用降至幾乎為零. + +然後我們必須考慮前端介面, 網站及用戶介面提供訪問平台之機會. 以下為許多選項. 此平台持有者能夠限制介面訪問權並索取費用. 平台擁有者也可以決定開放存取權限 (權力歸於人民!) 並讓任何人為平台建構介面。 或平台持有者能夠做出處於前兩選項之間之綜合選擇. + +_商業領導人具廣泛視野將知道如何商業化此. 目前我們所能視的為, 此與現狀不同, 且其可能有利可圖._ + +甚至, 其具自動化功能及多種支付角度來檢視問題. 有些東西可以非常[有效地代幣化](https://hackernoon.com/tokenization-of-digital-assets-g0ffk3v8s?ref=hackernoon.com),並在分類廣告板上交易。 代幣化資產能簡單被交易於區塊鏈中. 高強度支付手段能被簡單導入至一區塊鏈. + +聞到商業機會了嗎? 一分類訊息版無一運作成本並能被簡單導入, 包括複雜支付方案於各類交易. 我們很確定未來將會有更多有趣創想來更加擴張此用途. + +我們只是很高興能建造此. 來一起看看其程式程式碼吧. + +## 實作 {#implementation} + +不久前,我們啟動了一個[開源儲存庫](https://github.com/HQ20/contracts?ref=hackernoon.com),其中包含商業案例的實作範例和其他好東西,歡迎查看。 + +這個 [以太坊分類廣告板](https://github.com/HQ20/contracts/tree/master/contracts/classifieds?ref=hackernoon.com) 的程式碼就在那裡,請盡情使用。 只是請小心某些程式還未被完全審核, 所以你需謹慎檢查研究當投資資產於此. + +分類訊息版之基礎核心其實相當簡單. 所有廣告於分類版為建構於以下幾行字段: + +```solidity +struct Trade { + address poster; + uint256 item; + uint256 price; + bytes32 status; // Open, Executed, Cancelled +} +``` + +所以當某人公開此一廣告. item為販售物件. price為物件價格. status表示物件狀態為公開, 執行, 或取消. + +所有交易將被管理於一擬地圖/mapping結構. 因為所有物件於Solidity需要被標示類似地圖映射. 加上此管理類型十分方便. + +```solidity +mapping(uint256 => Trade) public trades; +``` + +使用一mapping代表我們需要設置一id來為所有想公開之廣告, 而我們也須事前瞭解一廣告id來實際執行此. 其有多種類型方案來處理此於智慧型合約或前端介面. 如你有任何不明點, 請自由發問或查看相關幫助資訊. + +接下來我們須考慮和物件需要被處理, 並指定其支付貨幣為何. + +對於這些項目,我們只要求它們實作 [ERC-721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol?ref=hackernoon.com) 介面,這實際上只是在區塊鏈中表示現實世界物品的一種方式,儘管它[最適用於數位資產](https://hackernoon.com/tokenization-of-digital-assets-g0ffk3v8s?ref=hackernoon.com)。 我們將必須創建一ERC-721合約於建立架構, 代表其任何資產於分類訊息版需要事前被代幣化. + +為所有支付, 我們需要進行一類似之程序. 大多數區塊鏈專案都定義了自己的 [ERC-20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol?ref=hackernoon.com) 加密貨幣。 有些傾向使用主流選項如DAI. 於此分類資訊版, 你需要來決定你加密貨幣之建立基礎架構為何. 簡單吧. + +```solidity +constructor ( + address _currencyTokenAddress, address _itemTokenAddress +) public { + currencyToken = IERC20(_currencyTokenAddress); + itemToken = IERC721(_itemTokenAddress); + tradeCounter = 0; +} +``` + +就快到了!! 我們有了廣告, 交易物件, 及支付貨幣. 來建立一廣告代表需要放置資產於質押狀態, 以顯示你確實擁有此並無雙重公開此於其他分類訊息版. + +以下程式運作此質押功能. 放置物件於質押, 創建一廣告, 做些記帳操作. + +```solidity +function openTrade(uint256 _item, uint256 _price) + public +{ + itemToken.transferFrom(msg.sender, address(this), _item); + trades[tradeCounter] = Trade({ + poster: msg.sender, + item: _item, + price: _price, + status: "Open" + }); + tradeCounter += 1; + emit TradeStatusChange(tradeCounter - 1, "Open"); +} +``` + +來接收交易代表選擇一廣告(交易), 支付其價格, 並接收物件. 此程式代表獲取一交易. 查看其是否為可供用狀態. 支付物件價格. 獲取物件. 更新廣告狀態. + +```solidity +function executeTrade(uint256 _trade) + public +{ + Trade memory trade = trades[_trade]; + require(trade.status == "Open", "Trade is not Open."); + currencyToken.transferFrom(msg.sender, trade.poster, trade.price); + itemToken.transferFrom(address(this), msg.sender, trade.item); + trades[_trade].status = "Executed"; + emit TradeStatusChange(_trade, "Executed"); +} +``` + +最終, 我們將具一功能使賣家能於買家接收前退出交易. 於一些模式, 廣告將於過期前存留一段時間. 你的選擇, 基於你市場之設計. + +此程式非常類似於執行一交易, 不過此不具交換貨幣, 且物件回返至廣告公布者. + +```solidity +function cancelTrade(uint256 _trade) + public +{ + Trade memory trade = trades[_trade]; + require( + msg.sender == trade.poster, + "交易只能由張貼者取消。" + ); + require(trade.status == "Open", "交易未開啟。"); + itemToken.transferFrom(address(this), trade.poster, trade.item); + trades[_trade].status = "Cancelled"; + emit TradeStatusChange(_trade, "Cancelled"); +} +``` + +此為全部程式碼!! 你以完成所有所需導入步驟. 此為非常驚人當一些商業概念被表達於程式程式碼, 而以上為其中一範例. 請在[我們的儲存庫](https://github.com/HQ20/contracts/blob/master/contracts/classifieds/Classifieds.sol)中查看完整的合約。 + +## 結論 {#conclusion} + +分類信息板是一種常見商業模式, 其於網路技術之幫助下, 大規模擴張的市場結構, 但此也是一種容易形成少數壟斷贏家的非常流行的商業模式. + +分類信息板也恰好是在區塊鏈環境中容易進行複制的一種簡單工具, 其具有可以挑戰現有的巨頭的非常具體之功能. + +在本文中,我們嘗試將分類信息板的商業業務與技術實現共同進行講解. 如果你擁有合適的技能, 這些知識應該可以幫助你創建願景及實施路線藍圖. + +一如往常,如果您想打造一些有趣的專案並需要一些建議,請[與我聯絡](https://albertocuesta.es/)! 我們隨時樂意幫助你. diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-mint-an-nft/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-mint-an-nft/index.md new file mode 100644 index 00000000000..0ee8cf69785 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-mint-an-nft/index.md @@ -0,0 +1,329 @@ +--- +title: "如何鑄造 NFT (NFT 教學系列 2/3)" +description: "本教學說明如何在以太坊區塊鏈上,使用我們的智能合約和 Web3 來鑄造 NFT。" +author: "Sumi Mudgil" +tags: [ "ERC-721", "alchemy", "solidity", "smart contracts" ] +skill: beginner +lang: zh-tw +published: 2021-04-22 +--- + +[Beeple](https://www.nytimes.com/2021/03/11/arts/design/nft-auction-christies-beeple.html):6900 萬美元 +[3LAU](https://www.forbes.com/sites/abrambrown/2021/03/03/3lau-nft-nonfungible-tokens-justin-blau/?sh=5f72ef64643b):1100 萬美元 +[Grimes](https://www.theguardian.com/music/2021/mar/02/grimes-sells-digital-art-collection-non-fungible-tokens):600 萬美元 + +他們全都使用 Alchemy 強大的 API 鑄造了他們的 NFT。 在本教學中,我們將教您如何在 \<10 分鐘內完成同樣的操作。 + +「鑄造 NFT」是在區塊鏈上發佈您 ERC-721 代幣的獨特實例的行為。 使用我們在 [本 NFT 教學系列第 1 部分](/developers/tutorials/how-to-write-and-deploy-an-nft/) 中的智能合約,讓我們大展 Web3 身手,鑄造一個 NFT。 在本教學結束時,您將能夠鑄造您內心 (和錢包) 渴望的任意數量 NFT! + +讓我們開始吧! + +## 第 1 步:安裝 Web3 {#install-web3} + +如果您跟隨了關於創建 NFT 智能合約的第一個教學,那麼您已經有使用 Ethers.js 的經驗了。 Web3 與 Ethers 類似,它是一個函式庫,可用來更輕鬆地向以太坊區塊鏈發出請求。 在本教學中,我們將使用 [Alchemy Web3](https://docs.alchemyapi.io/alchemy/documentation/alchemy-web3),這是一個增強型的 Web3 函式庫,提供自動重試和穩健的 WebSocket 支援。 + +在您的專案主目錄中執行: + +``` +npm install @alch/alchemy-web3 +``` + +## 第 2 步:建立 `mint-nft.js` 檔案 {#create-mintnftjs} + +在您的 scripts 目錄中,建立一個 `mint-nft.js` 檔案並新增以下程式碼: + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) +``` + +## 第 3 步:取得您的合約 ABI {#contract-abi} + +我們的合約 ABI (應用程式二進位介面) 是與我們的智能合約互動的介面。 您可以在[此處](https://docs.alchemyapi.io/alchemy/guides/eth_getlogs#what-are-ab-is)深入了解合約 ABI。 Hardhat 會自動為我們產生一個 ABI,並將其儲存在 `MyNFT.json` 檔案中。 為了使用它,我們需要將以下程式碼新增至我們的 `mint-nft.js` 檔案來解析內容: + +```js +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +``` + +如果您想查看 ABI,可以將其輸出到您的主控台: + +```js +console.log(JSON.stringify(contract.abi)) +``` + +若要執行 `mint-nft.js` 並在主控台中查看您的 ABI,請前往您的終端機並執行: + +```js +node scripts/mint-nft.js +``` + +## 第 4 步:使用 IPFS 設定 NFT 的元數據 {#config-meta} + +如果您還記得我們在第 1 部分的教學,我們的 `mintNFT` 智能合約函數會接收一個 `tokenURI` 參數,該參數應解析為一個描述 NFT 元數據的 JSON 文件 — 元數據是真正賦予 NFT 生命的東西,允許它擁有可設定的屬性,例如名稱、描述、圖片和其他屬性。 + +> _星際檔案系統 (IPFS) 是一種去中心化協定和點對點網路,用於在分散式檔案系統中儲存和共享資料。_ + +我們將使用 Pinata 這個方便的 IPFS API 和工具組,來儲存我們的 NFT 資產和元數據,以確保我們的 NFT 是真正去中心化的。 如果您沒有 Pinata 帳戶,請[在此](https://app.pinata.cloud)註冊一個免費帳戶,並完成步驟來驗證您的電子郵件。 + +建立帳戶後: + +- 前往「檔案」頁面,然後點擊頁面左上方的藍色「上傳」按鈕。 + +- 將圖片上傳到 Pinata — 這將是您 NFT 的圖片資產。 您可以隨意為資產命名。 + +- 上傳後,您會在「檔案」頁面的表格中看到檔案資訊。 您還會看到一個 CID 欄位。 您可以點擊旁邊的複製按鈕來複製 CID。 您可以在 `https://gateway.pinata.cloud/ipfs/` 查看您上傳的內容。 例如,您可以在[此處](https://gateway.pinata.cloud/ipfs/QmZdd5KYdCFApWn7eTZJ1qgJu18urJrP9Yh1TZcZrZxxB5)找到我們在 IPFS 上使用的圖片。 + +為方便視覺型學習者,以上步驟總結如下: + +![如何將您的圖片上傳到 Pinata](./instructionsPinata.gif) + +現在,我們要再上傳一份文件到 Pinata。 但在這麼做之前,我們需要先建立它! + +在您的根目錄中,建立一個名為 `nft-metadata.json` 的新檔案,並新增以下 json 程式碼: + +```json +{ + "attributes": [ + { + "trait_type": "品種", + "value": "瑪爾濟斯貴賓犬" + }, + { + "trait_type": "眼睛顏色", + "value": "摩卡色" + } + ], + "description": "全世界最可愛、最敏感的小狗。", + "image": "ipfs://QmWmvTJmJU3pozR9ZHFmQC2DNDwi2XJtf3QGyYiiagFSWb", + "name": "Ramses" +} +``` + +您可以隨意變更 json 中的資料。 您可以移除或新增屬性區段。 最重要的是,請確保圖片欄位指向您 IPFS 圖片的位置 — 否則,您的 NFT 將包含一張 (非常可愛的!) 狗的照片。 狗。 + +完成編輯 JSON 檔案後,儲存並上傳到 Pinata,步驟與我們上傳圖片時相同。 + +![如何將您的 nft-metadata.json 上傳到 Pinata](./uploadPinata.gif) + +## 第 5 步:建立您合約的實例 {#instance-contract} + +現在,為了與我們的合約互動,我們需要在程式碼中建立它的實例。 為此,我們需要我們的合約地址,可以從部署中取得,或透過 [Blockscout](https://eth-sepolia.blockscout.com/) 查詢您用來部署合約的地址來取得。 + +![在 Etherscan 上查看您的合約地址](./view-contract-etherscan.png) + +在上面的範例中,我們的合約地址是 0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778。 + +接下來,我們將使用 Web3 的[合約方法](https://docs.web3js.org/api/web3-eth-contract/class/Contract),利用 ABI 和地址來建立我們的合約。 在您的 `mint-nft.js` 檔案中,新增以下內容: + +```js +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" + +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) +``` + +## 第 6 步:更新 `.env` 檔案 {#update-env} + +現在,為了建立交易並傳送到以太坊鏈,我們將使用您的公開以太坊帳戶地址來取得帳戶 nonce (稍後會解釋)。 + +將您的公鑰新增到 `.env` 檔案 — 如果您完成了教學的第 1 部分,我們的 `.env` 檔案現在應該會像這樣: + +```js +API_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key" +PRIVATE_KEY = "your-private-account-address" +PUBLIC_KEY = "your-public-account-address" +``` + +## 第 7 步:建立您的交易 {#create-txn} + +首先,我們來定義一個名為 `mintNFT(tokenData)` 的函式,並透過以下步驟建立我們的交易: + +1. 從 `.env` 檔案中取得您的 _PRIVATE_KEY_ 和 _PUBLIC_KEY_。 + +2. 接下來,我們需要找出帳戶 nonce。 nonce 規範是用來追蹤從您的地址傳送的交易數量 — 我們需要它來確保安全並防止[重放攻擊](https://docs.alchemyapi.io/resources/blockchain-glossary#account-nonce)。 若要取得從您的地址傳送的交易數量,我們使用 [getTransactionCount](https://docs.alchemyapi.io/documentation/alchemy-api-reference/json-rpc#eth_gettransactioncount)。 + +3. 最後,我們將使用以下資訊來設定我們的交易: + +- `'from': PUBLIC_KEY` — 我們交易的來源是我們的公開地址 + +- `'to': contractAddress` — 我們希望與之互動並傳送交易的合約 + +- `'nonce': nonce` — 帳戶 nonce,其中包含從我們地址傳送的交易數量 + +- `'gas': estimatedGas` — 完成交易預計所需的 Gas + +- `'data': nftContract.methods.mintNFT(PUBLIC_KEY, md).encodeABI()` — 我們希望在此交易中執行的運算 — 在本例中是鑄造一個 NFT + +您的 `mint-nft.js` 檔案現在應該會像這樣: + +```js + require('dotenv').config(); + const API_URL = process.env.API_URL; + const PUBLIC_KEY = process.env.PUBLIC_KEY; + const PRIVATE_KEY = process.env.PRIVATE_KEY; + + const { createAlchemyWeb3 } = require("@alch/alchemy-web3"); + const web3 = createAlchemyWeb3(API_URL); + + const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json"); + const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778"; + const nftContract = new web3.eth.Contract(contract.abi, contractAddress); + + async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, 'latest'); // 取得最新的 nonce + + // 交易 + const tx = { + 'from': PUBLIC_KEY, + 'to': contractAddress, + 'nonce': nonce, + 'gas': 500000, + 'data': nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI() + }; + }​ +``` + +## 第 8 步:簽署交易 {#sign-txn} + +既然我們已經建立了交易,就需要簽署它才能將其傳送出去。 這就是我們要使用私鑰的地方。 + +`web3.eth.sendSignedTransaction` 會提供給我們交易哈希,我們可以用它來確保我們的交易已被挖出,且沒有被網路丟棄。 您會注意到,在交易簽署部分,我們新增了一些錯誤檢查,以便我們知道交易是否成功完成。 + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") // 取得最新的 nonce + + // 交易 + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "您的交易哈希為:", + hash, + "\n請查看 Alchemy 的 Mempool 以檢視您交易的狀態!" + ) + } else { + console.log( + "提交您的交易時發生錯誤:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log(" 承諾失敗:", err) + }) +} +``` + +## 第 9 步:呼叫 `mintNFT` 並執行 node `mint-nft.js` {#call-mintnft-fn} + +還記得您上傳到 Pinata 的 `metadata.json` 嗎? 從 Pinata 取得其哈希碼,並將以下內容作為參數傳遞給 `mintNFT` 函式 `https://gateway.pinata.cloud/ipfs/` + +以下是如何取得哈希碼: + +![如何在 Pinata 上取得您的 nft 元數據哈希碼](./metadataPinata.gif)_如何在 Pinata 上取得您的 nft 元數據哈希碼_ + +> 透過在獨立視窗中載入 `https://gateway.pinata.cloud/ipfs/`,再次確認您複製的哈希碼連結到您的 **metadata.json**。 頁面應與下方的螢幕截圖類似: + +![您的頁面應顯示 json 元數據](./metadataJSON.png)_您的頁面應顯示 json 元數據_ + +總而言之,您的程式碼看起來應該像這樣: + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") // 取得最新的 nonce + + // 交易 + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "您的交易哈希為:", + hash, + "\n請查看 Alchemy 的 Mempool 以檢視您交易的狀態!" + ) + } else { + console.log( + "提交您的交易時發生錯誤:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log("承諾失敗:", err) + }) +} + +mintNFT("ipfs://QmYueiuRNmL4MiA2GwtVMm6ZagknXnSpQnB3z2gWbz36hP") +``` + +現在,執行 `node scripts/mint-nft.js` 來部署您的 NFT。 幾秒鐘後,您應該會在終端機中看到類似這樣的回應: + + ``` + 您的交易哈希為:0x301791fdf492001fcd9d5e5b12f3aa1bbbea9a88ed24993a8ab2cdae2d06e1e8 + + 請查看 Alchemy 的 Mempool 以檢視您交易的狀態! + ``` + +接下來,造訪您的 [Alchemy mempool](https://dashboard.alchemyapi.io/mempool) 查看您交易的狀態 (無論是待處理、已挖出或被網路丟棄)。 如果您的交易被丟棄,查看 [Blockscout](https://eth-sepolia.blockscout.com/) 並搜尋您的交易哈希也會有幫助。 + +![在 Etherscan 上檢視您的 NFT 交易哈希](./view-nft-etherscan.png)_在 Etherscan 上檢視您的 NFT 交易哈希_ + +就這樣! 您現在已經在以太坊區塊鏈上部署並鑄造了一個 NFT + +使用 `mint-nft.js`,您可以隨心所欲 (錢包也允許的話) 地鑄造任意數量的 NFT! 只要確保傳入一個新的 tokenURI 來描述 NFT 的元數據即可 (否則,您最終只會製造出一堆 ID 不同但內容相同的 NFT)。 + +想必您會希望能在錢包中展示您的 NFT — 所以請務必查看 [第 3 部分:如何在您的錢包中檢視您的 NFT](/developers/tutorials/how-to-view-nft-in-metamask/)! diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md new file mode 100644 index 00000000000..3564c2533b9 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md @@ -0,0 +1,102 @@ +--- +title: "如何模擬 Solidity 智能合約進行測試" +description: "為什麼在測試時應該要取笑你的合約" +author: Markus Waas +lang: zh-tw +tags: [ "solidity", "smart contracts", "testing", "mocking" ] +skill: intermediate +published: 2020-05-02 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/mocking-contracts +--- + +[模擬物件](https://wikipedia.org/wiki/Mock_object) 是物件導向編程中一種常見的設計模式。 這個詞源自古法語單字「mocquer」,意思是「取笑」,後來演變成「模仿真實的東西」,這正是我們在編程中所做的事。 如果你想的話,可以盡情取笑你的智能合約,但只要可以,就請模擬它們。 這能讓你的日子更輕鬆。 + +## 使用模擬進行合約的單元測試 {#unit-testing-contracts-with-mocks} + +模擬合約基本上是指建立該合約的第二個版本,其行為與原始版本非常相似,但開發者能輕易控制其行為。 你經常會遇到複雜的合約,而你只想[對合約的一小部分進行單元測試](/developers/docs/smart-contracts/testing/)。 問題是,如果測試這一小部分需要一個很難達到的特定合約狀態,該怎麼辦? + +你可以每次都編寫複雜的測試設定邏輯,將合約帶入所需的狀態,或者你也可以編寫一個模擬。 透過繼承來模擬合約很簡單。 只要建立一個繼承自原始合約的第二個模擬合約即可。 現在你可以為你的模擬覆寫函數。 讓我們來看一個例子。 + +## 範例:私有 ERC20 {#example-private-erc20} + +我們使用一個範例 ERC-20 合約,它有一個初始的私有時間。 擁有者可以管理私有使用者,而且一開始只有這些使用者可以接收代幣。 一旦經過特定時間,所有人都可以使用這些代幣。 如果你感到好奇,我們使用的是新版 OpenZeppelin 合約 v3 中的 [`_beforeTokenTransfer`](https://docs.openzeppelin.com/contracts/5.x/extending-contracts#using-hooks) 掛鉤。 + +```solidity +pragma solidity ^0.6.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract PrivateERC20 is ERC20, Ownable { + mapping (address => bool) public isPrivateUser; + uint256 private publicAfterTime; + + constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public { + publicAfterTime = now + privateERC20timeInSec; + } + + function addUser(address user) external onlyOwner { + isPrivateUser[user] = true; + } + + function isPublic() public view returns (bool) { + return now >= publicAfterTime; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + require(_validRecipient(to), "PrivateERC20:無效的接收者"); + } + + function _validRecipient(address to) private view returns (bool) { + if (isPublic()) { + return true; + } + + return isPrivateUser[to]; + } +} +``` + +現在讓我們來模擬它。 + +```solidity +pragma solidity ^0.6.0; +import "../PrivateERC20.sol"; + +contract PrivateERC20Mock is PrivateERC20 { + bool isPublicConfig; + + constructor() public PrivateERC20(0) {} + + function setIsPublic(bool isPublic) external { + isPublicConfig = isPublic; + } + + function isPublic() public view returns (bool) { + return isPublicConfig; + } +} +``` + +你會收到以下其中一則錯誤訊息: + +- `PrivateERC20Mock.sol: TypeError: Overriding function is missing "override" specifier.` +- `PrivateERC20.sol: TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?.` + +由於我們使用的是新版 Solidity 0.6,我們必須為可被覆寫的函數加上 `virtual` 關鍵字,並為進行覆寫的函數加上 `override` 關鍵字。 所以讓我們將這些關鍵字加到兩個 `isPublic` 函數中。 + +現在,在你的單元測試中,你可以改用 `PrivateERC20Mock`。 當你想在私有使用期間測試其行為時,請使用 `setIsPublic(false)`;同樣地,測試公開使用期間時請用 `setIsPublic(true)`。 當然,在我們的範例中,我們也可以使用[時間輔助工具](https://docs.openzeppelin.com/test-helpers/0.5/api#increase)來相應地更改時間。 但現在模擬的概念應該很清楚了,你可以想像在某些情境下,事情並不像單純推進時間那麼簡單。 + +## 模擬多個合約 {#mocking-many-contracts} + +如果你必須為每一個模擬都建立另一個合約,事情可能會變得很混亂。 如果這困擾著你,你可以看看 [MockContract](https://github.com/gnosis/mock-contract) 函式庫。 它讓你能即時覆寫和改變合約的行為。 然而,它只適用於模擬對另一個合約的呼叫,所以在我們的範例中行不通。 + +## 模擬的功能可以更強大 {#mocking-can-be-even-more-powerful} + +模擬的功能不止於此。 + +- 新增函數:不僅覆寫特定函數很有用,單純新增額外函數也同樣有用。 一個關於代幣的好例子是,增加一個額外的 `mint` 函數,讓任何使用者都能免費獲得新代幣。 +- 在測試網上使用:當你在測試網上部署和測試你的合約以及去中心化應用程序時,請考慮使用模擬合約。 除非真的有必要,否則請避免覆寫函數。 畢竟,你想要測試的是真實的邏輯。 但舉例來說,新增一個重設函數可能很有用,它可以簡單地將合約狀態重設回初始狀態,而無需重新部署。 當然,你不會想在主網合約中新增這樣的函式。 diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md new file mode 100644 index 00000000000..49ce8b604e7 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md @@ -0,0 +1,702 @@ +--- +title: "如何使用 Echidna 測試智能合約" +description: "如何使用 Echidna 自動測試智能合約" +author: "Trailofbits" +lang: zh-tw +tags: [ "solidity", "smart contracts", "security", "testing", "fuzzing" ] +skill: advanced +published: 2020-04-10 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/echidna +--- + +## 安裝 {#installation} + +Echidna 可透過 docker 或使用預先編譯的二進位檔進行安裝。 + +### 透過 docker 使用 Echidna {#echidna-through-docker} + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox +``` + +_最後一個指令會在可存取您目前目錄的 docker 容器中執行 eth-security-toolbox。 您可以從主機變更檔案,並從 docker 在檔案上執行工具_ + +在 docker 中執行: + +```bash +solc-select 0.5.11 +cd /home/training +``` + +### 二進位檔 {#binary} + +[https://github.com/crytic/echidna/releases/tag/v1.4.0.0](https://github.com/crytic/echidna/releases/tag/v1.4.0.0) + +## 基於屬性的模糊測試簡介 {#introduction-to-property-based-fuzzing} + +Echidna 是一款基於屬性的模糊測試器,我們在之前的部落格文章中介紹過 ([1](https://blog.trailofbits.com/2018/03/09/echidna-a-smart-fuzzer-for-ethereum/)、[2](https://blog.trailofbits.com/2018/05/03/state-machine-testing-with-echidna/)、[3](https://blog.trailofbits.com/2020/03/30/an-echidna-for-all-seasons/))。 + +### 模糊測試 {#fuzzing} + +[模糊測試](https://wikipedia.org/wiki/Fuzzing) 是資安社群中一項眾所周知的技術。 它包含產生或多或少隨機的輸入,以尋找程式中的錯誤。 傳統軟體的模糊測試器 (例如 [AFL](http://lcamtuf.coredump.cx/afl/) 或 [LibFuzzer](https://llvm.org/docs/LibFuzzer.html)) 是眾所周知的有效尋找錯誤工具。 + +除了純粹隨機產生輸入之外,還有許多技術和策略可以產生良好的輸入,包括: + +- 從每次執行中取得回饋,並利用它來引導產生過程。 例如,如果一個新產生的輸入導致發現一條新路徑,那麼在其附近產生新的輸入可能是有意義的。 +- 產生符合結構性約束的輸入。 例如,如果您的輸入包含帶有校驗和的標頭,讓模糊測試器產生能驗證校驗和的輸入會更有意義。 +- 使用已知的輸入來產生新的輸入:如果您可以存取大量的有效輸入資料集,您的模糊測試器可以從中產生新的輸入,而無需從頭開始。 這些通常被稱為 _種子_。 + +### 基於屬性的模糊測試 {#property-based-fuzzing} + +Echidna 屬於一類特殊的模糊測試器:基於屬性的模糊測試,其靈感主要來自 [QuickCheck](https://wikipedia.org/wiki/QuickCheck)。 與試圖尋找當機的傳統模糊測試器不同,Echidna 會嘗試破壞使用者定義的不變量。 + +在智能合約中,不變量是 Solidity 函數,可以表示合約可能達到的任何不正確或無效的狀態,包括: + +- 不正確的存取控制:攻擊者成為合約的擁有者。 +- 不正確的狀態機器:在合約暫停時,代幣仍可被轉移。 +- 不正確的算術:使用者可以讓其餘額下溢,並獲得無限的免費代幣。 + +### 使用 Echidna 測試屬性 {#testing-a-property-with-echidna} + +我們將了解如何使用 Echidna 測試智能合約。 目標是以下的智能合約 [`token.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/token.sol): + +```solidity +contract Token{ + mapping(address => uint) public balances; + function airdrop() public{ + balances[msg.sender] = 1000; + } + function consume() public{ + require(balances[msg.sender]>0); + balances[msg.sender] -= 1; + } + function backdoor() public{ + balances[msg.sender] += 1; + } +} +``` + +我們將假設此代幣必須具有以下屬性: + +- 任何人最多只能擁有 1000 個代幣 +- 此代幣無法轉移 (它不是 ERC20 代幣) + +### 撰寫屬性 {#write-a-property} + +Echidna 屬性是 Solidity 函數。 一個屬性必須: + +- 沒有參數 +- 如果成功,則返回 `true` +- 其名稱以 `echidna` 開頭 + +Echidna 會: + +- 自動產生任意交易來測試屬性。 +- 回報任何導致屬性返回 `false` 或拋出錯誤的交易。 +- 在呼叫屬性時捨棄副作用 (亦即,如果屬性改變了一個狀態變數,它會在測試後被捨棄) + +以下屬性會檢查呼叫者擁有的代幣數量不超過 1000 個: + +```solidity +function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; +} +``` + +使用繼承將您的合約與您的屬性分開: + +```solidity +contract TestToken is Token{ + function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; + } + } +``` + +[`token.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/token.sol) 實作了該屬性並繼承自該代幣。 + +### 初始化合約 {#initiate-a-contract} + +Echidna 需要一個沒有參數的[建構函式](/developers/docs/smart-contracts/anatomy/#constructor-functions)。 如果您的合約需要特定的初始化,您需要在建構函式中進行。 + +在 Echidna 中有一些特定的地址: + +- `0x00a329c0648769A73afAc7F9381E08FB43dBEA72`,它會呼叫建構函式。 +- `0x10000`、`0x20000` 和 `0x00a329C0648769a73afAC7F9381e08fb43DBEA70`,它們會隨機呼叫其他函數。 + +在我們目前的範例中,我們不需要任何特定的初始化,因此我們的建構函式是空的。 + +### 執行 Echidna {#run-echidna} + +Echidna 的啟動方式如下: + +```bash +echidna-test contract.sol +``` + +如果 contract.sol 包含多個合約,您可以指定目標: + +```bash +echidna-test contract.sol --contract MyContract +``` + +### 摘要:測試屬性 {#summary-testing-a-property} + +以下總結了 echidna 在我們範例上的執行情況: + +```solidity +contract TestToken is Token{ + constructor() public {} + function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; + } + } +``` + +```bash +echidna-test testtoken.sol --contract TestToken +... + +echidna_balance_under_1000: failed!💥 + Call sequence, shrinking (1205/5000): + airdrop() + backdoor() + +... +``` + +Echidna 發現如果呼叫了 `backdoor`,屬性就會被違反。 + +## 在模糊測試活動中過濾要呼叫的函數 {#filtering-functions-to-call-during-a-fuzzing-campaign} + +我們將了解如何過濾要進行模糊測試的函數。 +目標是以下的智能合約: + +```solidity +contract C { + bool state1 = false; + bool state2 = false; + bool state3 = false; + bool state4 = false; + + function f(uint x) public { + require(x == 12); + state1 = true; + } + + function g(uint x) public { + require(state1); + require(x == 8); + state2 = true; + } + + function h(uint x) public { + require(state2); + require(x == 42); + state3 = true; + } + + function i() public { + require(state3); + state4 = true; + } + + function reset1() public { + state1 = false; + state2 = false; + state3 = false; + return; + } + + function reset2() public { + state1 = false; + state2 = false; + state3 = false; + return; + } + + function echidna_state4() public returns (bool) { + return (!state4); + } +} +``` + +這個小範例迫使 Echidna 尋找特定的交易序列來改變一個狀態變數。 +這對於模糊測試器來說很困難 (建議使用像 [Manticore](https://github.com/trailofbits/manticore) 這樣的符號執行工具)。 +我們可以執行 Echidna 來驗證這一點: + +```bash +echidna-test multi.sol +... +echidna_state4: passed! 🎉 +Seed: -3684648582249875403 +``` + +### 過濾函數 {#filtering-functions} + +Echidna 很難找到正確的序列來測試此合約,因為兩個重設函數 (`reset1` 和 `reset2`) 會將所有狀態變數設為 `false`。 +然而,我們可以使用一個特殊的 Echidna 功能,將重設函數列入黑名單,或只將 `f`、`g`、 +`h` 和 `i` 函數列入白名單。 + +要將函數列入黑名單,我們可以使用此設定檔: + +```yaml +filterBlacklist: true +filterFunctions: ["reset1", "reset2"] +``` + +過濾函數的另一種方法是列出白名單中的函數。 為此,我們可以使用此設定檔: + +```yaml +filterBlacklist: false +filterFunctions: ["f", "g", "h", "i"] +``` + +- `filterBlacklist` 預設為 `true`。 +- 過濾將只依名稱進行 (不含參數)。 如果您有 `f()` 和 `f(uint256)`,過濾器 `"f"` 將會匹配這兩個函數。 + +### 執行 Echidna {#run-echidna-1} + +要使用設定檔 `blacklist.yaml` 執行 Echidna: + +```bash +echidna-test multi.sol --config blacklist.yaml +... +echidna_state4: failed!💥 + Call sequence: + f(12) + g(8) + h(42) + i() +``` + +Echidna 將幾乎立即找到可證偽該屬性的交易序列。 + +### 摘要:過濾函數 {#summary-filtering-functions} + +在模糊測試活動中,Echidna 可以使用以下方式將函數列入黑名單或白名單: + +```yaml +filterBlacklist: true +filterFunctions: ["f1", "f2", "f3"] +``` + +```bash +echidna-test contract.sol --config config.yaml +... +``` + +Echidna 會根據 `filterBlacklist` 布林值的值,開始一場模糊測試活動,將 `f1`、`f2` 和 `f3` 列入黑名單或只呼叫這些函數。 + +## 如何使用 Echidna 測試 Solidity 的 assert {#how-to-test-soliditys-assert-with-echidna} + +在這個簡短的教學中,我們將展示如何使用 Echidna 來測試合約中的斷言檢查。 假設我們有這樣一個合約: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + // tmp <= counter + return (counter - tmp); + } +} +``` + +### 撰寫斷言 {#write-an-assertion} + +我們希望確保在返回其差值後,`tmp` 小於或等於 `counter`。 我們可以撰寫一個 +Echidna 屬性,但我們需要將 `tmp` 值儲存在某個地方。 相反地,我們可以使用像這樣的斷言: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + assert (tmp <= counter); + return (counter - tmp); + } +} +``` + +### 執行 Echidna {#run-echidna-2} + +要啟用斷言失敗測試,請建立一個 [Echidna 設定檔](https://github.com/crytic/echidna/wiki/Config) `config.yaml`: + +```yaml +checkAsserts: true +``` + +當我們在 Echidna 中執行此合約時,我們會得到預期的結果: + +```bash +echidna-test assert.sol --config config.yaml +Analyzing contract: assert.sol:Incrementor +assertion in inc: failed!💥 + Call sequence, shrinking (2596/5000): + inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) + inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) + inc(86844066927987146567678238756515930889952488499230423029593188005934847229952) + +Seed: 1806480648350826486 +``` + +如您所見,Echidna 在 `inc` 函數中回報了一些斷言失敗。 每個函數可以新增多個斷言,但 Echidna 無法判斷是哪一個斷言失敗。 + +### 何時以及如何使用斷言 {#when-and-how-use-assertions} + +斷言可以用作明確屬性的替代方案,特別是當要檢查的條件與某個操作 `f` 的正確使用直接相關時。 在一些程式碼之後加入斷言,將強制在它執行後立即進行檢查: + +```solidity +function f(..) public { + // 一些複雜的程式碼 + ... + assert (condition); + ... +} + +``` + +相反地,使用明確的 echidna 屬性會隨機執行交易,並且沒有簡單的方法可以強制確切的檢查時機。 仍然可以透過這個變通方法來達成: + +```solidity +function echidna_assert_after_f() public returns (bool) { + f(..); + return(condition); +} +``` + +然而,這會有一些問題: + +- 如果 `f` 被宣告為 `internal` 或 `external`,它會失敗。 +- 不清楚應該使用哪些參數來呼叫 `f`。 +- 如果 `f` 回復,屬性將會失敗。 + +一般而言,我們建議遵循 [John Regehr 的建議](https://blog.regehr.org/archives/1091) 來使用斷言: + +- 在斷言檢查期間不要強制產生任何副作用。 例如:`assert(ChangeStateAndReturn() == 1)` +- 不要斷言顯而易見的陳述。 例如 `assert(var >= 0)`,其中 `var` 被宣告為 `uint`。 + +最後,請**不要**使用 `require` 來代替 `assert`,因為 Echidna 將無法偵測到它 (但合約無論如何都會回復)。 + +### 摘要:斷言檢查 {#summary-assertion-checking} + +以下總結了 echidna 在我們範例上的執行情況: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + assert (tmp <= counter); + return (counter - tmp); + } +} +``` + +```bash +echidna-test assert.sol --config config.yaml +Analyzing contract: assert.sol:Incrementor +assertion in inc: failed!💥 + Call sequence, shrinking (2596/5000): + inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) + inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) + inc(86844066927987146567678238756515930889952488499230423029593188005934847229952) + +Seed: 1806480648350826486 +``` + +Echidna 發現如果 `inc` 函數被多次以大參數呼叫,其中的斷言可能會失敗。 + +## 收集與修改 Echidna 語料庫 {#collecting-and-modifying-an-echidna-corpus} + +我們將了解如何使用 Echidna 收集和使用交易語料庫。 目標是以下的智能合約 [`magic.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/magic.sol): + +```solidity +contract C { + bool value_found = false; + function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public { + require(magic_1 == 42); + require(magic_2 == 129); + require(magic_3 == magic_4+333); + value_found = true; + return; + } + + function echidna_magic_values() public returns (bool) { + return !value_found; + } + +} +``` + +這個小範例迫使 Echidna 尋找特定值來改變狀態變數。 這對於模糊測試器來說很困難 +(建議使用像 [Manticore](https://github.com/trailofbits/manticore) 這樣的符號執行工具)。 +我們可以執行 Echidna 來驗證這一點: + +```bash +echidna-test magic.sol +... + +echidna_magic_values: passed! 🎉 + +Seed: 2221503356319272685 +``` + +然而,我們仍然可以在執行此模糊測試活動時使用 Echidna 來收集語料庫。 + +### 收集語料庫 {#collecting-a-corpus} + +要啟用語料庫收集,請建立一個語料庫目錄: + +```bash +mkdir corpus-magic +``` + +以及一個 [Echidna 設定檔](https://github.com/crytic/echidna/wiki/Config) `config.yaml`: + +```yaml +coverage: true +corpusDir: "corpus-magic" +``` + +現在我們可以執行我們的工具並檢查收集到的語料庫: + +```bash +echidna-test magic.sol --config config.yaml +``` + +Echidna 仍然找不到正確的魔術值,但我們可以看看它收集的語料庫。 +例如,其中一個檔案是: + +```json +[ + { + "_gas'": "0xffffffff", + "_delay": ["0x13647", "0xccf6"], + "_src": "00a329c0648769a73afac7f9381e08fb43dbea70", + "_dst": "00a329c0648769a73afac7f9381e08fb43dbea72", + "_value": "0x0", + "_call": { + "tag": "SolCall", + "contents": [ + "magic", + [ + { + "contents": [ + 256, + "93723985220345906694500679277863898678726808528711107336895287282192244575836" + ], + "tag": "AbiUInt" + }, + { + "contents": [256, "334"], + "tag": "AbiUInt" + }, + { + "contents": [ + 256, + "68093943901352437066264791224433559271778087297543421781073458233697135179558" + ], + "tag": "AbiUInt" + }, + { + "tag": "AbiUInt", + "contents": [256, "332"] + } + ] + ] + }, + "_gasprice'": "0xa904461f1" + } +] +``` + +顯然,這個輸入不會觸發我們屬性中的失敗。 然而,在下一步中,我們將看到如何為此修改它。 + +### 為語料庫提供種子 {#seeding-a-corpus} + +Echidna 需要一些幫助才能處理 `magic` 函數。 我們將複製並修改輸入,為其使用合適的 +參數: + +```bash +cp corpus/2712688662897926208.txt corpus/new.txt +``` + +我們將修改 `new.txt` 來呼叫 `magic(42,129,333,0)`。 現在,我們可以重新執行 Echidna: + +```bash +echidna-test magic.sol --config config.yaml +... +echidna_magic_values: failed!💥 + Call sequence: + magic(42,129,333,0) + + +Unique instructions: 142 +Unique codehashes: 1 +Seed: -7293830866560616537 + +``` + +這一次,它立即發現與該屬性發生了衝突。 + +## 尋找高 gas 消耗的交易 {#finding-transactions-with-high-gas-consumption} + +我們將了解如何使用 Echidna 找到高 gas 消耗的交易。 目標是以下的智能合約: + +```solidity +contract C { + uint state; + + function expensive(uint8 times) internal { + for(uint8 i=0; i < times; i++) + state = state + i; + } + + function f(uint x, uint y, uint8 times) public { + if (x == 42 && y == 123) + expensive(times); + else + state = 0; + } + + function echidna_test() public returns (bool) { + return true; + } + +} +``` + +這裡的 `expensive` 可能會有大量的 gas 消耗。 + +目前,Echidna 總是需要一個屬性來測試:這裡 `echidna_test` 總是返回 `true`。 +我們可以執行 Echidna 來驗證這一點: + +``` +echidna-test gas.sol +... +echidna_test: passed! 🎉 + +Seed: 2320549945714142710 +``` + +### 測量 Gas 消耗 {#measuring-gas-consumption} + +要透過 Echidna 啟用 gas 消耗測量,請建立一個設定檔 `config.yaml`: + +```yaml +estimateGas: true +``` + +在此範例中,我們還將縮小交易序列的大小,使結果更易於理解: + +```yaml +seqLen: 2 +estimateGas: true +``` + +### 執行 Echidna {#run-echidna-3} + +建立設定檔後,我們可以像這樣執行 Echidna: + +```bash +echidna-test gas.sol --config config.yaml +... +echidna_test: passed! 🎉 + +f used a maximum of 1333608 gas + Call sequence: + f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2 + +Unique instructions: 157 +Unique codehashes: 1 +Seed: -325611019680165325 + +``` + +- 顯示的 gas 是由 [HEVM](https://github.com/dapphub/dapptools/tree/master/src/hevm#hevm-) 提供的估計值。 + +### 過濾掉減少 Gas 的呼叫 {#filtering-out-gas-reducing-calls} + +上方關於**在模糊測試活動中過濾要呼叫的函數**的教學展示了如何 +從您的測試中移除一些函數。 +這對於獲得準確的 gas 估計值至關重要。 +請參考以下範例: + +```solidity +contract C { + address [] addrs; + function push(address a) public { + addrs.push(a); + } + function pop() public { + addrs.pop(); + } + function clear() public{ + addrs.length = 0; + } + function check() public{ + for(uint256 i = 0; i < addrs.length; i++) + for(uint256 j = i+1; j < addrs.length; j++) + if (addrs[i] == addrs[j]) + addrs[j] = address(0x0); + } + function echidna_test() public returns (bool) { + return true; + } +} +``` + +如果 Echidna 可以呼叫所有函數,它將不易找到高 gas 成本的交易: + +``` +echidna-test pushpop.sol --config config.yaml +... +pop used a maximum of 10746 gas +... +check used a maximum of 23730 gas +... +clear used a maximum of 35916 gas +... +push used a maximum of 40839 gas +``` + +這是因為成本取決於 `addrs` 的大小,且隨機呼叫往往會讓陣列幾乎是空的。 +然而,將 `pop` 和 `clear` 列入黑名單,會給我們帶來好得多的結果: + +```yaml +filterBlacklist: true +filterFunctions: ["pop", "clear"] +``` + +``` +echidna-test pushpop.sol --config config.yaml +... +push used a maximum of 40839 gas +... +check used a maximum of 1484472 gas +``` + +### 摘要:尋找高 gas 消耗的交易 {#summary-finding-transactions-with-high-gas-consumption} + +Echidna 可以使用 `estimateGas` 設定選項來尋找高 gas 消耗的交易: + +```yaml +estimateGas: true +``` + +```bash +echidna-test contract.sol --config config.yaml +... +``` + +一旦模糊測試活動結束,Echidna 將回報每個函數具有最大 gas 消耗的序列。 diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md new file mode 100644 index 00000000000..29c7c79dfc4 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md @@ -0,0 +1,517 @@ +--- +title: "如何使用 Manticore 尋找智能合約中的程式錯誤" +description: "如何使用 Manticore 自動尋找智能合約中的程式錯誤" +author: Trailofbits +lang: zh-tw +tags: [ "solidity", "smart contracts", "security", "testing", "formal verification" ] +skill: advanced +published: 2020-01-13 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/manticore +--- + +本教學旨在說明如何使用 Manticore 自動尋找智能合約中的程式錯誤。 + +## 安裝 {#installation} + +Manticore 需要 python 3.6 或以上版本。 可以透過 pip 或使用 docker 安裝。 + +### 透過 docker 使用 Manticore {#manticore-through-docker} + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox +``` + +_最後一個指令會在可存取您目前目錄的 docker 容器中執行 eth-security-toolbox。 您可以從主機變更檔案,並從 docker 在檔案上執行工具_ + +在 docker 中,執行: + +```bash +solc-select 0.5.11 +cd /home/trufflecon/ +``` + +### 透過 pip 使用 Manticore {#manticore-through-pip} + +```bash +pip3 install --user manticore +``` + +建議使用 solc 0.5.11。 + +### 執行腳本 {#running-a-script} + +若要使用 python 3 執行 python 腳本: + +```bash +python3 script.py +``` + +## 動態符號執行簡介 {#introduction-to-dynamic-symbolic-execution} + +### 動態符號執行概覽 {#dynamic-symbolic-execution-in-a-nutshell} + +動態符號執行 (DSE) 是一種程式分析技術,以高度語意感知的方式探索狀態空間。 這項技術基於「程式路徑」的發現,表示為稱為 `path predicates` (路徑謂詞) 的數學公式。 從概念上講,這項技術分兩步驟對路徑謂詞進行操作: + +1. 它們是利用程式輸入的約束條件建構的。 +2. 它們被用來生成程式輸入,從而觸發相關路徑的執行。 + +這種方法不會產生誤報,因為所有識別出的程式狀態都可以在具體執行期間被觸發。 例如,如果分析發現整數溢位,保證可以重現。 + +### 路徑謂詞範例 {#path-predicate-example} + +為了深入了解 DSE 的工作原理,請參考以下範例: + +```solidity +function f(uint a){ + + if (a == 65) { + // 存在程式錯誤 + } + +} +``` + +由於 `f()` 包含兩個路徑,DSE 將建構兩個不同的路徑謂詞: + +- 路徑 1:`a == 65` +- 路徑 2:`Not (a == 65)` + +每個路徑謂詞都是一個數學公式,可以交給所謂的 [SMT 求解器](https://wikipedia.org/wiki/Satisfiability_modulo_theories),求解器會嘗試解出方程式。 對於`路徑 1`,求解器會說可以用 `a = 65` 探索該路徑。 對於`路徑 2`,求解器可以為 `a` 指定任何非 65 的值,例如 `a = 0`。 + +### 驗證屬性 {#verifying-properties} + +Manticore 允許完全控制每個路徑的執行。 因此,它允許您對幾乎任何東西添加任意約束。 這種控制允許在合約上創建屬性。 + +請參考以下範例: + +```solidity +function unsafe_add(uint a, uint b) returns(uint c){ + c = a + b; // 沒有溢位保護 + return c; +} +``` + +這裡函數中只有一個路徑可供探索: + +- 路徑 1:`c = a + b` + +使用 Manticore,您可以檢查溢位,並將約束添加到路徑謂詞中: + +- `c = a + b AND (c < a OR c < b)` + +如果能找到 `a` 和 `b` 的一個賦值,使得上述路徑謂詞可行,那就表示您發現了一個溢位。 例如,求解器可以產生輸入 `a = 10, b = MAXUINT256`。 + +如果您考慮一個修復後的版本: + +```solidity +function safe_add(uint a, uint b) returns(uint c){ + c = a + b; + require(c>=a); + require(c>=b); + return c; +} +``` + +帶有溢位檢查的相關公式會是: + +- `c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)` + +這個公式無法求解;換句話說,這**證明**了在 `safe_add` 中,`c` 將永遠增加。 + +因此,DSE 是一個強大的工具,可以驗證您程式碼上的任意約束。 + +## 在 Manticore 下執行 {#running-under-manticore} + +我們將了解如何使用 Manticore API 探索智能合約。 目標是以下智能合約 [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; + +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### 執行獨立探索 {#run-a-standalone-exploration} + +您可以透過以下指令直接在智能合約上執行 Manticore (`project` 可以是 Solidity 檔案或專案目錄): + +```bash +$ manticore project +``` + +您將獲得類似這樣的測試案例輸出 (順序可能會改變): + +``` +... +... m.c.manticore:INFO: Generated testcase No. 0 - STOP +... m.c.manticore:INFO: Generated testcase No. 1 - REVERT +... m.c.manticore:INFO: Generated testcase No. 2 - RETURN +... m.c.manticore:INFO: Generated testcase No. 3 - REVERT +... m.c.manticore:INFO: Generated testcase No. 4 - STOP +... m.c.manticore:INFO: Generated testcase No. 5 - REVERT +... m.c.manticore:INFO: Generated testcase No. 6 - REVERT +... m.c.manticore:INFO: Results in /home/ethsec/workshops/Automated Smart Contracts Audit - TruffleCon 2018/manticore/examples/mcore_t6vi6ij3 +... +``` + +若無額外資訊,Manticore 將使用新的符號交易來探索合約,直到合約中沒有新的路徑可供探索。 Manticore 在一次失敗的交易後 (例如:在 revert 之後) 不會執行新的交易。 + +Manticore 會將資訊輸出到 `mcore_*` 目錄中。 除此之外,您會在這個目錄中找到: + +- `global.summary`:涵蓋範圍和編譯器警告 +- `test_XXXXX.summary`:每個測試案例的涵蓋範圍、最後指令、帳戶餘額 +- `test_XXXXX.tx`:每個測試案例的詳細交易清單 + +這裡 Manticore 找到了 7 個測試案例,它們對應於 (檔案名稱順序可能會改變): + +| | 交易 0 | 交易 1 | 交易 2 | 結果 | +| :-------------------------------------------------------: | :--: | :------------------------: | -------------------------- | :----: | +| **test_00000000.tx** | 合約創建 | f(!=65) | f(!=65) | STOP | +| **test_00000001.tx** | 合約創建 | 遞補函數 | | REVERT | +| **test_00000002.tx** | 合約創建 | | | RETURN | +| **test_00000003.tx** | 合約創建 | f(65) | | REVERT | +| **test_00000004.tx** | 合約創建 | f(!=65) | | STOP | +| **test_00000005.tx** | 合約創建 | f(!=65) | f(65) | REVERT | +| **test_00000006.tx** | 合約創建 | f(!=65) | 遞補函數 | REVERT | + +_探索摘要 f(!=65) 表示以任何不同於 65 的值呼叫 f。_ + +如您所見,Manticore 會為每個成功或回復的交易產生唯一的測試案例。 + +如果您想要快速探索程式碼,請使用 `--quick-mode` 旗標 (它會停用程式錯誤偵測器、Gas 計算...) + +### 透過 API 操作智能合約 {#manipulate-a-smart-contract-through-the-api} + +本節詳細說明如何透過 Manticore Python API 操作智能合約。 您可以建立副檔名為 `*.py` 的新檔案,並透過將 API 指令 (其基本原理將在下面說明) 加入到這個檔案中來撰寫必要的程式碼,然後使用 `$ python3 *.py` 指令執行它。 您也可以直接在 python 主控台執行以下指令,使用 `$ python3` 指令執行主控台。 + +### 建立帳戶 {#creating-accounts} + +您應該做的第一件事是使用以下指令啟動新的區塊鏈: + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() +``` + +使用 [m.create_account](https://manticore.readthedocs.io/en/latest/evm.html?highlight=create_account#manticore.ethereum.ManticoreEVM.create_account) 建立非合約帳戶: + +```python +user_account = m.create_account(balance=1000) +``` + +可以使用 [m.solidity_create_contract](https://manticore.readthedocs.io/en/latest/evm.html?highlight=solidity_create#manticore.ethereum.ManticoreEVM.create_contract) 部署 Solidity 合約: + +```solidity +source_code = ''' +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +''' +# 啟動合約 +contract_account = m.solidity_create_contract(source_code, owner=user_account) +``` + +#### 總結 {#summary} + +- 您可以使用 [m.create_account](https://manticore.readthedocs.io/en/latest/evm.html?highlight=create_account#manticore.ethereum.ManticoreEVM.create_account) 和 [m.solidity_create_contract](https://manticore.readthedocs.io/en/latest/evm.html?highlight=solidity_create#manticore.ethereum.ManticoreEVM.create_contract) 建立使用者帳戶和合約帳戶。 + +### 執行交易 {#executing-transactions} + +Manticore 支援兩種類型的交易: + +- 原始交易:探索所有函數 +- 具名交易:只探索一個函數 + +#### 原始交易 {#raw-transaction} + +使用 [m.transaction](https://manticore.readthedocs.io/en/latest/evm.html?highlight=transaction#manticore.ethereum.ManticoreEVM.transaction) 執行原始交易: + +```python +m.transaction(caller=user_account, + address=contract_account, + data=data, + value=value) +``` + +交易的呼叫者、位址、資料或值可以是具體的或符號性的: + +- [m.make_symbolic_value](https://manticore.readthedocs.io/en/latest/evm.html?highlight=make_symbolic_value#manticore.ethereum.ManticoreEVM.make_symbolic_value) 會建立一個符號值。 +- [m.make_symbolic_buffer(size)](https://manticore.readthedocs.io/en/latest/evm.html?highlight=make_symbolic_buffer#manticore.ethereum.ManticoreEVM.make_symbolic_buffer) 會建立一個符號位元組陣列。 + +例如: + +```python +symbolic_value = m.make_symbolic_value() +symbolic_data = m.make_symbolic_buffer(320) +m.transaction(caller=user_account, + address=contract_address, + data=symbolic_data, + value=symbolic_value) +``` + +如果資料是符號性的,Manticore 將在交易執行期間探索合約的所有函數。 查看 [Hands on the Ethernaut CTF](https://blog.trailofbits.com/2017/11/06/hands-on-the-ethernaut-ctf/) 文章中的遞補函數說明,將有助於了解函數選擇的運作方式。 + +#### 具名交易 {#named-transaction} + +函數可以透過其名稱執行。 +若要從 user_account 以一個符號值和 0 以太幣執行 `f(uint var)`,請使用: + +```python +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var, caller=user_account, value=0) +``` + +如果未指定交易的 `value`,則預設為 0。 + +#### 摘要 {#summary-1} + +- 交易的參數可以是具體的或符號性的 +- 原始交易將探索所有函數 +- 函數可以依其名稱呼叫 + +### 工作區 {#workspace} + +`m.workspace` 是用於所有生成檔案的輸出目錄: + +```python +print("Results are in {}".format(m.workspace)) +``` + +### 終止探索 {#terminate-the-exploration} + +若要停止探索,請使用 [m.finalize()](https://manticore.readthedocs.io/en/latest/evm.html?highlight=finalize#manticore.ethereum.ManticoreEVM.finalize)。 一旦呼叫此方法,就不應再傳送任何交易,Manticore 會為每個探索的路徑產生測試案例。 + +### 摘要:在 Manticore 下執行 {#summary-running-under-manticore} + +將所有先前的步驟放在一起,我們得到: + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() + +with open('example.sol') as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +print("Results are in {}".format(m.workspace)) +m.finalize() # 停止探索 +``` + +您可以在 [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) 中找到上述所有程式碼 + +## 取得擲回路徑 {#getting-throwing-paths} + +現在我們將為 `f()` 中引發例外的路徑產生特定輸入。 目標仍然是以下智能合約 [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### 使用狀態資訊 {#using-state-information} + +每個執行的路徑都有其區塊鏈的狀態。 一個狀態要麼是就緒的,要麼是被終止的,這意味著它到達了 THROW 或 REVERT 指令: + +- [m.ready_states](https://manticore.readthedocs.io/en/latest/states.html#accessing):就緒狀態的清單 (它們沒有執行 REVERT/INVALID) +- [m.killed_states](https://manticore.readthedocs.io/en/latest/states.html#accessings):被終止的狀態清單 +- [m.all_states](https://manticore.readthedocs.io/en/latest/states.html#accessings):所有狀態 + +```python +for state in m.all_states: + # 對狀態執行某些操作 +``` + +您可以存取狀態資訊。 例如: + +- `state.platform.get_balance(account.address)`:帳戶的餘額 +- `state.platform.transactions`:交易清單 +- `state.platform.transactions[-1].return_data`:最後一筆交易傳回的資料 + +最後一筆交易傳回的資料是一個陣列,可以使用 ABI.deserialize 將其轉換為一個值,例如: + +```python +data = state.platform.transactions[0].return_data +data = ABI.deserialize("uint", data) +``` + +### 如何產生測試案例 {#how-to-generate-testcase} + +使用 [m.generate_testcase(state, name)](https://manticore.readthedocs.io/en/latest/evm.html?highlight=generate_testcase#manticore.ethereum.ManticoreEVM.generate_testcase) 來產生測試案例: + +```python +m.generate_testcase(state, 'BugFound') +``` + +### 摘要 {#summary-2} + +- 您可以使用 m.all_states 疊代狀態 +- `state.platform.get_balance(account.address)` 會傳回帳戶的餘額 +- `state.platform.transactions` 會傳回交易清單 +- `transaction.return_data` 是傳回的資料 +- `m.generate_testcase(state, name)` 為狀態產生輸入 + +### 摘要:取得擲回路徑 {#summary-getting-throwing-path} + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() + +with open('example.sol') as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +## 檢查執行是否以 REVERT 或 INVALID 結束 + +for state in m.terminated_states: + last_tx = state.platform.transactions[-1] + if last_tx.result in ['REVERT', 'INVALID']: + print('Throw found {}'.format(m.workspace)) + m.generate_testcase(state, 'ThrowFound') +``` + +您可以在 [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) 中找到上述所有程式碼 + +_注意:我們本可以產生一個更簡單的腳本,因為 terminated_state 傳回的所有狀態在其結果中都有 REVERT 或 INVALID:這個範例只是為了示範如何操作 API。_ + +## 新增約束 {#adding-constraints} + +我們將了解如何約束探索。 我們將假設 `f()` 的文件說明該函數永遠不會以 `a == 65` 呼叫,因此任何與 `a == 65` 相關的程式錯誤都不是真正的程式錯誤。 目標仍然是以下智能合約 [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### 運算子 {#operators} + +[Operators](https://github.com/trailofbits/manticore/blob/master/manticore/core/smtlib/operators.py) 模組有助於操作約束,它提供了以下等功能: + +- Operators.AND, +- Operators.OR, +- Operators.UGT (無符號大於), +- Operators.UGE (無符號大於或等於), +- Operators.ULT (無符號小於), +- Operators.ULE (無符號小於或等於)。 + +若要匯入該模組,請使用以下指令: + +```python +from manticore.core.smtlib import Operators +``` + +`Operators.CONCAT` 用於將陣列串接到一個值。 例如,交易的 return_data 需要更改為一個值,以便與另一個值進行檢查: + +```python +last_return = Operators.CONCAT(256, *last_return) +``` + +### 約束 {#state-constraint} + +您可以全域或針對特定狀態使用約束。 + +#### 全域約束 {#state-constraint} + +使用 `m.constrain(constraint)` 來新增全域約束。 +例如,您可以從一個符號位址呼叫合約,並將此位址限制為特定值: + +```python +symbolic_address = m.make_symbolic_value() +m.constraint(Operators.OR(symbolic == 0x41, symbolic_address == 0x42)) +m.transaction(caller=user_account, + address=contract_account, + data=m.make_symbolic_buffer(320), + value=0) +``` + +#### 狀態約束 {#state-constraint} + +使用 [state.constrain(constraint)](https://manticore.readthedocs.io/en/latest/states.html?highlight=StateBase#manticore.core.state.StateBase.constrain) 將約束新增到特定狀態。 +它可用於在探索後約束狀態,以檢查其上的某些屬性。 + +### 檢查約束 {#checking-constraint} + +使用 `solver.check(state.constraints)` 來了解約束是否仍然可行。 +例如,以下將約束 symbolic_value 不等於 65,並檢查狀態是否仍然可行: + +```python +state.constrain(symbolic_var != 65) +if solver.check(state.constraints): + # 狀態可行 +``` + +### 摘要:新增約束 {#summary-adding-constraints} + +將約束新增到先前的程式碼中,我們得到: + +```python +from manticore.ethereum import ManticoreEVM +from manticore.core.smtlib.solver import Z3Solver + +solver = Z3Solver.instance() + +m = ManticoreEVM() + +with open("example.sol") as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +no_bug_found = True + +## 檢查執行是否以 REVERT 或 INVALID 結束 + +for state in m.terminated_states: + last_tx = state.platform.transactions[-1] + if last_tx.result in ['REVERT', 'INVALID']: + # 我們不考慮 a == 65 的路徑 + condition = symbolic_var != 65 + if m.generate_testcase(state, name="BugFound", only_if=condition): + print(f'找到程式錯誤,結果位於 {m.workspace}') + no_bug_found = False + +if no_bug_found: + print(f'未找到程式錯誤') +``` + +您可以在 [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) 中找到上述所有程式碼 diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md new file mode 100644 index 00000000000..9ca5ae60633 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md @@ -0,0 +1,233 @@ +--- +title: "如何使用 Slither 來尋找智能合約漏洞" +description: "如何使用 Slither 自動尋找智能合約中的漏洞" +author: Trailofbits +lang: zh-tw +tags: [ "solidity", "smart contracts", "security", "testing" ] +skill: advanced +published: 2020-06-09 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/slither +--- + +## 如何使用 Slither {#how-to-use-slither} + +本教學旨在說明如何使用 Slither 自動尋找智能合約中的漏洞。 + +- [安裝](#installation) +- [命令列用法](#command-line) +- [靜態分析簡介](#static-analysis):靜態分析簡介 +- [API](#api-basics):Python API 說明 + +## 安裝 {#installation} + +Slither 需要 Python >= 3.6。 可以透過 pip 或使用 docker 安裝。 + +透過 pip 安裝 Slither: + +```bash +pip3 install --user slither-analyzer +``` + +透過 docker 安裝 Slither: + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox +``` + +_最後一個指令會在可存取您目前目錄的 docker 容器中執行 eth-security-toolbox。 您可以從主機變更檔案,並從 docker 在檔案上執行工具_ + +在 docker 中,執行: + +```bash +solc-select 0.5.11 +cd /home/trufflecon/ +``` + +### 執行腳本 {#running-a-script} + +若要使用 python 3 執行 python 腳本: + +```bash +python3 script.py +``` + +### 命令列 {#command-line} + +**命令列與使用者定義的腳本。** Slither 內建一組預先定義的偵測器,可尋找許多常見漏洞。 從命令列呼叫 Slither 將會執行所有偵測器,無需具備靜態分析的詳細知識: + +```bash +slither project_paths +``` + +除了偵測器之外,Slither 還透過其 [printers](https://github.com/crytic/slither#printers) 和 [tools](https://github.com/crytic/slither#tools) 具備程式碼審查功能。 + +使用 [crytic.io](https://github.com/crytic) 來存取私有偵測器和 GitHub 整合功能。 + +## 靜態分析 {#static-analysis} + +Slither 靜態分析框架的功能與設計,已在部落格文章 ([1](https://blog.trailofbits.com/2018/10/19/slither-a-solidity-static-analysis-framework/)、[2](https://blog.trailofbits.com/2019/05/27/slither-the-leading-static-analyzer-for-smart-contracts/)) 與一篇 [學術論文](https://github.com/trailofbits/publications/blob/master/papers/wetseb19.pdf) 中有所說明。 + +靜態分析存在多種類型。 您很可能知道,像 [clang](https://clang-analyzer.llvm.org/) 和 [gcc](https://lwn.net/Articles/806099/) 這類的編譯器都依賴這些研究技術,但它同時也是 ([Infer](https://fbinfer.com/)、[CodeClimate](https://codeclimate.com/)、[FindBugs](http://findbugs.sourceforge.net/) 以及像 [Frama-C](https://frama-c.com/) 和 [Polyspace](https://www.mathworks.com/products/polyspace.html) 這類基於形式化方法的工具的基礎。 + +在此我們不會詳盡地探討靜態分析技術與研究者。 反之,我們將著重於了解 Slither 的運作原理,以便您能更有效地利用它來尋找漏洞並理解程式碼。 + +- [程式碼表示法](#code-representation) +- [程式碼分析](#analysis) +- [中介表示法](#intermediate-representation) + +### 程式碼表示法 {#code-representation} + +動態分析推論單一執行路徑,與之相對的是,靜態分析一次推論所有路徑。 為此,它依賴於一種不同的程式碼表示法。 兩種最常見的是抽象語法樹 (AST) 以及控制流程圖 (CFG)。 + +### 抽象語法樹 (AST) {#abstract-syntax-trees-ast} + +每當編譯器解析程式碼時,都會使用 AST。 這或許是執行靜態分析所能依據的最基本結構。 + +簡而言之,AST 是一種結構化樹,其中通常每個葉節點包含一個變數或一個常數,而內部節點則是運算元或控制流程操作。 請看以下程式碼: + +```solidity +function safeAdd(uint a, uint b) pure internal returns(uint){ + if(a + b <= a){ + revert(); + } + return a + b; +} +``` + +對應的 AST 如下圖所示: + +![AST](./ast.png) + +Slither 使用由 solc 匯出的 AST。 + +雖然 AST 建構簡單,但它是一個巢狀結構。 有時,這並非最直接易於分析的結構。 例如,要識別表達式 `a + b <= a` 中使用的操作,您必須先分析 `<=`,然後再分析 `+`。 常見的方法是使用所謂的「訪問者模式」,此模式會遞迴地遍歷樹狀結構。 Slither 在 [`ExpressionVisitor`](https://github.com/crytic/slither/blob/master/slither/visitors/expression/expression.py) 中包含一個通用的訪問者。 + +以下程式碼使用 `ExpressionVisitor` 來偵測表達式是否包含加法: + +```python +from slither.visitors.expression.expression import ExpressionVisitor +from slither.core.expressions.binary_operation import BinaryOperationType + +class HasAddition(ExpressionVisitor): + + def result(self): + return self._result + + def _post_binary_operation(self, expression): + if expression.type == BinaryOperationType.ADDITION: + self._result = True + +visitor = HasAddition(expression) # expression 是要測試的表達式 +print(f'The expression {expression} has a addition: {visitor.result()}') +``` + +### 控制流程圖 (CFG) {#control-flow-graph-cfg} + +第二種最常見的程式碼表示法是控制流程圖 (CFG)。 顧名思義,這是一種以圖形為基礎的表示法,它揭示了所有的執行路徑。 每個節點包含一個或多個指令。 圖中的邊代表控制流程操作 (if/then/else、迴圈等)。 我們前述範例的 CFG 如下: + +![CFG](./cfg.png) + +CFG 是大多數分析所建構於其上的表示法。 + +還存在許多其他的程式碼表示法。 根據您想要執行的分析,每種表示法各有其優缺點。 + +### 分析 {#analysis} + +您可以使用 Slither 執行的最簡單的分析類型是語法分析。 + +### 語法分析 {#syntax-analysis} + +Slither 可以遍歷程式碼的不同元件及其表示法,以使用類似模式比對的方法來尋找不一致之處和缺陷。 + +例如,下列偵測器會尋找與語法相關的問題: + +- [狀態變數遮蔽](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variable-shadowing):疊代所有狀態變數,並檢查是否有任何變數遮蔽了繼承合約中的變數 ([state.py#L51-L62](https://github.com/crytic/slither/blob/0441338e055ab7151b30ca69258561a5a793f8ba/slither/detectors/shadowing/state.py#L51-L62)) + +- [不正確的 ERC20 介面](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc20-interface):尋找不正確的 ERC20 函式簽章 ([incorrect_erc20_interface.py#L34-L55](https://github.com/crytic/slither/blob/0441338e055ab7151b30ca69258561a5a793f8ba/slither/detectors/erc/incorrect_erc20_interface.py#L34-L55)) + +### 語意分析 {#semantic-analysis} + +與語法分析相比,語意分析會更深入地分析程式碼的「意義」。 這個類別包含一些廣泛的分析類型。 它們能產生更強大和有用的結果,但撰寫起來也更複雜。 + +語意分析被用於最進階的漏洞偵測。 + +#### 資料相依性分析 {#fixed-point-computation} + +如果存在一條路徑,其中 `variable_a` 的值受到 `variable_b` 的影響,則稱變數 `variable_a` 與 `variable_b` 具有資料相依性。 + +在以下程式碼中,`variable_a` 相依於 `variable_b`: + +```solidity +// ... +variable_a = variable_b + 1; +``` + +歸功於它的中介表示法 (將在後續章節討論),Slither 具備內建的 [資料相依性](https://github.com/crytic/slither/wiki/data-dependency) 分析功能。 + +資料相依性用法的一個範例可以在 [危險的嚴格相等偵測器](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities) 中找到。 在這裡,Slither 會尋找與危險值進行的嚴格相等比較 ([incorrect_strict_equality.py#L86-L87](https://github.com/crytic/slither/blob/6d86220a53603476f9567c3358524ea4db07fb25/slither/detectors/statements/incorrect_strict_equality.py#L86-L87)),並通知使用者應該使用 `>=` 或 `<=` 而不是 `==`,以防止攻擊者困住合約。 此外,偵測器會將對 `balanceOf(address)` 的呼叫的傳回值視為危險 ([incorrect_strict_equality.py#L63-L64](https://github.com/crytic/slither/blob/6d86220a53603476f9567c3358524ea4db07fb25/slither/detectors/statements/incorrect_strict_equality.py#L63-L64)),並使用資料相依性引擎來追蹤其用法。 + +#### 不動點運算 {#fixed-point-computation} + +如果您的分析遍歷 CFG 並沿著邊緣進行,您很可能會看到已經訪問過的節點。 例如,如果一個迴圈如下所示: + +```solidity +for(uint i; i < range; ++){ + variable_a += 1 +} +``` + +您的分析將需要知道何時停止。 這裡有兩種主要策略:(1) 在每個節點上疊代有限次數,(2) 計算所謂的「不動點」。 不動點基本上意味著分析此節點不再提供任何有意義的資訊。 + +不動點使用的一個範例可以在重入偵測器中找到:Slither 探索節點,尋找外部呼叫、寫入和讀取存儲。 一旦達到不動點 ([reentrancy.py#L125-L131](https://github.com/crytic/slither/blob/master/slither/detectors/reentrancy/reentrancy.py#L125-L131)),它會停止探索,並分析結果,透過不同的重入模式 ([reentrancy_benign.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_benign.py)、[reentrancy_read_before_write.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_read_before_write.py)、[reentrancy_eth.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_eth.py)) 來查看是否存在重入。 + +使用有效率的不動點運算來撰寫分析,需要充分了解分析如何傳播其資訊。 + +### 中介表示法 {#intermediate-representation} + +中介表示法 (IR) 是一種語言,意在使其比原始語言更適合進行靜態分析。 Slither 將 Solidity 轉譯為其自己的 IR:[SlithIR](https://github.com/crytic/slither/wiki/SlithIR)。 + +如果您只想撰寫基本的檢查,則無需了解 SlithIR。 然而,如果您計劃撰寫進階的語意分析,它將會非常方便。 [SlithIR](https://github.com/crytic/slither/wiki/Printer-documentation#slithir) 和 [SSA](https://github.com/crytic/slither/wiki/Printer-documentation#slithir-ssa) 列印器將幫助您了解程式碼是如何被轉譯的。 + +## 應用程式介面 (API) 基礎 {#api-basics} + +Slither 有一個 API,可讓您探索合約及其函式的基本屬性。 + +若要載入程式碼庫: + +```python +from slither import Slither +slither = Slither('/path/to/project') + +``` + +### 探索合約與函式 {#exploring-contracts-and-functions} + +一個 `Slither` 物件具有: + +- `contracts (list(Contract)`:合約清單 +- `contracts_derived (list(Contract)`:未被其他合約繼承的合約清單 (合約的子集) +- `get_contract_from_name (str)`:從名稱傳回一個合約 + +一個 `Contract` 物件具有: + +- `name (str)`:合約的名稱 +- `functions (list(Function))`:函式清單 +- `modifiers (list(Modifier))`:修飾符清單 +- `all_functions_called (list(Function/Modifier))`:合約可觸及的所有內部函式清單 +- `inheritance (list(Contract))`:繼承的合約清單 +- `get_function_from_signature (str)`:從簽章傳回一個函式 +- `get_modifier_from_signature (str)`:從簽章傳回一個修飾符 +- `get_state_variable_from_name (str)`:從名稱傳回一個 StateVariable + +一個 `Function` 或 `Modifier` 物件具有: + +- `name (str)`:函式的名稱 +- `contract (contract)`:宣告函式的合約 +- `nodes (list(Node))`:構成函式/修飾符 CFG 的節點清單 +- `entry_point (Node)`:CFG 的進入點 +- `variables_read (list(Variable))`:已讀取變數的清單 +- `variables_written (list(Variable))`:已寫入變數的清單 +- `state_variables_read (list(StateVariable))`:已讀取狀態變數的清單 (variables`read 的子集) +- `state_variables_written (list(StateVariable))`:已寫入狀態變數的清單 (variables`written 的子集) diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md new file mode 100644 index 00000000000..974d5f89105 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md @@ -0,0 +1,81 @@ +--- +title: "如何將 Tellor 設定為您的預言機" +description: "入門指南:將 Tellor 預言機整合至您的協定" +author: "Tellor" +lang: zh-tw +tags: [ "solidity", "smart contracts", "oracles" ] +skill: beginner +published: 2021-06-29 +source: Tellor Docs +sourceUrl: https://docs.tellor.io/tellor/ +--- + +隨堂測驗:您的協定即將完成,但需要一個預言機來存取鏈下資料...您該怎麼辦? + +## (軟性) 先決條件 {#soft-prerequisites} + +本篇文章旨在讓存取預言機資料流的過程盡可能簡單明瞭。 話雖如此,為了專注於預言機的相關內容,我們假設您具備以下程式設計能力。 + +假設: + +- 您會使用終端機 +- 您已安裝 npm +- 您知道如何使用 npm 來管理相依性 + +Tellor 是一個已上線且可供執行的開源預言機。 本入門指南將說明如何輕鬆啟用並執行 Tellor,為您的專案提供一個完全去中心化且抗審查的預言機。 + +## 概覽 {#overview} + +Tellor 是一個預言機系統,各方可以在系統中請求鏈下資料點 (例如 BTC/USD) 的值,而回報者則會競相將此值新增到鏈上資料庫,此資料庫可供所有以太坊智能合約存取。 此資料庫的輸入內容,由已質押回報者組成的網路保護。 Tellor 利用加密經濟激勵機制,獎勵誠實提交資料的回報者,並透過發行 Tellor 的代幣 Tributes (TRB) 以及爭議機制來懲罰惡意行為者。 + +在本教學中,我們將介紹: + +- 設定您啟用與執行時所需的初始工具組。 +- 逐步解說一個簡單範例。 +- 列出目前可供測試 Tellor 的網路所使用的測試網位址。 + +## UsingTellor {#usingtellor} + +您首先要做的是安裝使用 Tellor 作為預言機所需的基本工具。 使用[此套件](https://github.com/tellor-io/usingtellor) 來安裝 Tellor 使用者合約: + +`npm install usingtellor` + +安裝後,您的合約就能繼承「UsingTellor」合約中的函式。 + +太棒了! 現在您已準備好工具,讓我們透過一個簡單的練習來擷取比特幣價格: + +### BTC/USD 範例 {#btcusd-example} + +繼承 UsingTellor 合約,並將 Tellor 位址作為建構函式引數傳入: + +例如: + +```solidity +import "usingtellor/contracts/UsingTellor.sol"; + +contract PriceContract is UsingTellor { + uint256 public btcPrice; + + //此合約現在可以存取 UsingTellor 中的所有函式 + +constructor(address payable _tellorAddress) UsingTellor(_tellorAddress) public {} + +function setBtcPrice() public { + bytes memory _b = abi.encode("SpotPrice",abi.encode("btc","usd")); + bytes32 _queryId = keccak256(_b); + + uint256 _timestamp; + bytes _value; + + (_value, _timestamp) = getDataBefore(_queryId, block.timestamp - 15 minutes); + + btcPrice = abi.decode(_value,(uint256)); + } +} +``` + +如需完整的合約位址清單,請參閱[此處](https://docs.tellor.io/tellor/the-basics/contracts-reference)。 + +為方便使用,UsingTellor 儲存庫隨附 [Tellor Playground](https://github.com/tellor-io/TellorPlayground) 合約版本,以簡化整合。 請參閱[此處](https://github.com/tellor-io/sampleUsingTellor#tellor-playground)取得實用函式清單。 + +若要更穩健地執行 Tellor 預言機,請到[此處](https://github.com/tellor-io/usingtellor/blob/master/README.md)查看可用函式的完整清單。 diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-view-nft-in-metamask/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-view-nft-in-metamask/index.md new file mode 100644 index 00000000000..76357bc2aea --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-view-nft-in-metamask/index.md @@ -0,0 +1,33 @@ +--- +title: "如何在您的錢包中檢視 NFT (NFT 教學系列第 3/3 部分)" +description: "本教學說明如何在 MetaMask 上檢視現有的 NFT!" +author: "Sumi Mudgil" +tags: [ "ERC-721", "Alchemy", "Solidity" ] +skill: beginner +lang: zh-tw +published: 2021-04-22 +--- + +本教學是 NFT 教學系列的第 3/3 部分,我們將在此檢視我們新鑄造的 NFT。 不過,您也可以透過 MetaMask,將本通用教學應用於任何 ERC-721 代幣,包含在主網或任何測試網上。 如果您想學習如何在以太坊上鑄造自己的 NFT,請參閱[第 1 部分:如何編寫和部署 NFT 智能合約](/developers/tutorials/how-to-write-and-deploy-an-nft)! + +恭喜! 您已來到我們 NFT 教學系列中最短也最簡單的部分 — 如何在虛擬錢包中檢視您剛鑄造好的 NFT。 本範例將使用 MetaMask,因為我們在前兩個部分中也是使用它。 + +先決條件是,您應已在行動裝置上安裝 MetaMask,且其中應包含您鑄造 NFT 時所用的帳戶 — 您可以從 [iOS](https://apps.apple.com/us/app/metamask-blockchain-wallet/id1438144202) 或 [Android](https://play.google.com/store/apps/details?id=io.metamask&hl=en_US&gl=US) 免費取得此應用程式。 + +## 步驟 1:將您的網路設定為 Sepolia {#set-network-to-sepolia} + +在應用程式頂部,按下「錢包」按鈕,之後系統會提示您選取網路。 因為我們的 NFT 是在 Sepolia 網路上鑄造的,所以您需要選取 Sepolia 作為您的網路。 + +![如何在 MetaMask Mobile 上將 Sepolia 設定為您的網路](./goerliMetamask.gif) + +## 步驟 2:將您的收藏品新增至 MetaMask {#add-nft-to-metamask} + +進入 Sepolia 網路後,選取右側的「收藏品」標籤,然後新增您 NFT 的智能合約地址和 ERC-721 代幣 ID — 您應該能根據我們教學第二部分中部署 NFT 的交易哈希,在 Etherscan 上找到這些資訊。 + +![如何找到您的交易哈希和 ERC-721 代幣 ID](./findNFTEtherscan.png) + +您可能需要重新整理幾次才能檢視您的 NFT — 但它會在那裡 ! + +![如何將您的 NFT 上傳到 MetaMask](./findNFTMetamask.gif) + +恭喜! 您已成功鑄造一枚 NFT,現在可以檢視它了! 我們迫不及待想看到您將如何在 NFT 世界中掀起風潮! diff --git a/public/content/translations/zh-tw/developers/tutorials/how-to-write-and-deploy-an-nft/index.md b/public/content/translations/zh-tw/developers/tutorials/how-to-write-and-deploy-an-nft/index.md new file mode 100644 index 00000000000..e0db10d263c --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/how-to-write-and-deploy-an-nft/index.md @@ -0,0 +1,380 @@ +--- +title: "如何撰寫與部署 NFT (NFT 教學系列第 1/3 部分)" +description: "這篇教學是是一個關於NFT教學系列的文章之一,這將會帶著你一步步地學習如何撰寫與部署一個非同質化代幣 (ERC-721 代幣) 的智慧型合約在以太坊以及星際檔案系統(IPFS) 上" +author: "Sumi Mudgil" +tags: [ "ERC-721", "Alchemy", "Solidity", "smart contracts" ] +skill: beginner +lang: zh-tw +published: 2021-04-22 +--- + +隨著 NFT 將區塊鏈帶入公眾視野,現在正是您親自了解這股熱潮的絕佳機會,只要在以太坊區塊鏈上發佈您自己的 NFT 合約 (ERC-721 代幣) 即可! + +Alchemy 非常自豪能為 NFT 領域中最頂尖的品牌提供技術支援,包括 Makersplace (最近在佳士得拍賣會上以 6900 萬美元創下數位藝術品銷售記錄)、Dapper Labs (NBA Top Shot 和 CryptoKitties 的創造者)、OpenSea (全球最大的 NFT 市場)、Zora、Super Rare、NFTfi、Foundation、Enjin、Origin Protocol、Immutable 等等。 + +在本教學中,我們將逐步解說如何使用 [MetaMask](https://metamask.io/)、[Solidity](https://docs.soliditylang.org/en/v0.8.0/)、[Hardhat](https://hardhat.org/)、[Pinata](https://pinata.cloud/) 和 [Alchemy](https://alchemy.com/signup/eth) 在 Sepolia 測試網上建立與部署 ERC-721 智慧合約(如果您還不了解這些術語的含義,請別擔心——我們會一一解釋!)。 + +在這個教學的第二部分,我們將會瀏覽如何使用我們的智慧型合約去件至一個NFT,在第三部分我們將解釋如何在MeraMask查閱你的NFT。 + +當然,如果您在任何時候有任何問題,請隨時到 [Alchemy Discord](https://discord.gg/gWuC7zB) 提問,或造訪 [Alchemy 的 NFT API 文件](https://docs.alchemy.com/alchemy/enhanced-apis/nft-api)! + +## 第 1 步:連線至以太坊網路 {#connect-to-ethereum} + +有很多方法可以向以太坊區塊鏈發出請求,但為了簡化流程,我們將使用 [Alchemy](https://alchemy.com/signup/eth) 的免費帳戶。它是一個區塊鏈開發人員平台與 API,可讓我們在不需執行自有節點的情況下與以太坊鏈進行通訊。 + +在這個教學裡,我們也將會使用Alchemy的開發者工具監控與分析了解我們的智慧型合約部署方式 如果您還沒有 Alchemy 帳戶,可以點擊[此處](https://alchemy.com/signup/eth)免費註冊。 + +## 第 2 步:建立您的應用程式 (和 API 金鑰) {#make-api-key} + +一旦你已經創建好一個Alchemy的帳戶,你可以通過建立一個程式來生成一個API鑰匙。 這將讓我們能向 Sepolia 測試網發出請求。 如果您想深入了解測試網,請參閱[本指南](https://docs.alchemyapi.io/guides/choosing-a-network)。 + +1. 將滑鼠移至標題列上方的"Apps"以及點選"Create App"以前往到在你的Alchemy Dashboard 上的"Create App"頁面。 + +![建立您的應用程式](./create-your-app.png) + +2. 為您的應用程式命名 (我們選擇「My First NFT!」)、提供簡短描述、在「Chain」欄位選取「Ethereum」,並為您的網路選取「Sepolia」。 自「合併」後,其他測試網皆已棄用。 + +![設定並發布您的應用程式](./alchemy-explorer-sepolia.png) + +3. 點擊「創建程式」然後就好了! 你的程式應該會在下列圖表中出現。 + +## 第 3 步:建立以太坊帳戶 (地址) {#create-eth-address} + +我們需要一個乙太坊帳戶去接收或發送交易。 為此教學,我們將會使用 MetaMask。它是一個在瀏覽器上管理你的乙太坊帳戶地址的虛擬錢包。 如果您想深入了解以太坊上的交易如何運作,請參閱以太坊基金會的[此頁面](/developers/docs/transactions/)。 + +您可以在[這裡](https://metamask.io/download)免費下載並建立 MetaMask 帳戶。 建立帳戶時,或如果您已有帳戶,請務必在右上角切換至「Sepolia 測試網」(這樣我們就不用處理真實貨幣)。 + +![將 Sepolia 設為您的網路](./metamask-goerli.png) + +## 第 4 步:從水龍頭取得以太幣 {#step-4-add-ether-from-a-faucet} + +為了部屬我們的智慧型合約到測試網上,我們將會需要一些假的以太幣(ETH)。 若要取得 ETH,您可以前往由 Alchemy 託管的 [Sepolia Faucet](https://sepoliafaucet.com/),登入並輸入您的帳戶地址,然後點擊「Send Me ETH」。 接著你應蓋到你的MetaMask帳戶確認你的ETH! + +## 第 5 步:檢查您的餘額 {#check-balance} + +為了再次確認我們的餘額,我們將使用 [Alchemy 的編輯器工具](https://composer.alchemyapi.io?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D) 發出 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 請求。 這將會回傳你的錢包裡的餘額。 在你輸入自己的MetaMask帳戶地址,並且點下「寄送請求」後,你理應會看見一個這樣子的回應: + + ``` + `{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}` + ``` + +> **注意**:此結果以 wei 為單位,而非 ETH。 Wei是一個被用來計算以太最少分數的單位。 「1 eth = 1018 wei」他是這樣轉換的。 所以如果我們轉換0xde0b6b3a7640000到十進制,我們將會獲得1\*1018wei,這剛好是1ETH。 + +哈! 我們的假錢都在這。 + +## 第 6 步:初始化我們的專案 {#initialize-project} + +首先,我們需要一個資料夾給我們的專案。 前往到你的指令介面(powershell, cmd 或 Terminal) 接著輸入: + + ``` + mkdir my-nft + cd my-nft + ``` + +現在我們已經在我們的專案資料夾底下了,接著我們將會使用npm去初始化我們的專案。 如果您尚未安裝 npm,請遵循[這些指示](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm) (我們也需要 [Node.js](https://nodejs.org/en/download/),所以也請一併下載!)。 + + ``` + npm init + ``` + +你如何回答安裝問題並不重要,這是我們提供的參考: + +```json + package name: (my-nft) + version: (1.0.0) + description: My first NFT! + entry point: (index.js) + test command: + git repository: + keywords: + author: + license: (ISC) + About to write to /Users/thesuperb1/Desktop/my-nft/package.json: + + { + "name": "my-nft", + "version": "1.0.0", + "description": "My first NFT!", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" + } +``` + +同意創建package.json,接著我們已經準備好開始了! + +## 第 7 步:安裝 [Hardhat](https://hardhat.org/getting-started/#overview) {#install-hardhat} + +Hardhat 是一個開發環境,提供你去編譯、部屬、測試、以及除錯你的以太坊軟體。 它能協助開發人員在部署至即時鏈之前,於本機建立智慧合約和去中心化應用程式。 + +在我們的 my-nft 專案下執行: + + ``` + npm install --save-dev hardhat + ``` + +如需更多[安裝指示](https://hardhat.org/getting-started/#overview)的詳細資訊,請查看此頁面。 + +## 第 8 步:建立 Hardhat 專案 {#create-hardhat-project} + +在你的專案資料夾下執行: + + ``` + npx hardhat + ``` + +你接下來會看到歡迎訊息以及關於你想做什麼的選項。 選擇"create an empty hardhat.config.js": + + ``` + 888 888 888 888 888 + 888 888 888 888 888 + 888 888 888 888 888 + 8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 + 888 888 "88b 888P" d88" 888 888 "88b "88b 888 + 888 888 .d888888 888 888 888 888 888 .d888888 888 + 888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. + 888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + 👷 Welcome to Hardhat v2.0.11 👷‍ + ? What do you want to do? … + Create a sample project + ❯ Create an empty hardhat.config.js + Quit + ``` + +這將會摻生一個 hardhat.config.js file 給我們。 + +## 第 9 步:新增專案資料夾 {#add-project-folders} + +為了保持我們的資料夾的結構性,我們將會創建兩個資料夾。 在你的指令介面返回到到專案資料夾,接著輸入: + + ``` + mkdir contracts + mkdir scripts + ``` + +- contracts/ 是放置我們智慧型合約程式碼的地方 + +- scripts/ 是我們部屬我們的智慧型合約的地方 + +## 第 10 步:撰寫我們的合約 {#write-contract} + +現在我們的環境已經設定好了,接下來是更令人興奮的部分:_撰寫我們的智慧合約程式碼!_ + +在您偏好的編輯器中開啟 my-nft 專案 (我們推薦 [VSCode](https://code.visualstudio.com/))。 我們撰寫智慧型合約的語言稱作 Solidity 這將是我們使用去撰寫 MyNFT.sol智慧型合約。 + +1. 前往 `contracts` 資料夾,並建立一個名為 MyNFT.sol 的新檔案 + +2. 以下是我們的 NFT 智慧合約程式碼,此程式碼以 [OpenZeppelin](https://docs.openzeppelin.com/contracts/3.x/erc721) 函式庫的 ERC-721 實作為基礎。 複製與貼上下面的內容到你的MyNFT.sol檔案。 + + ```solidity + //合約基於 [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721) + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + + import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + import "@openzeppelin/contracts/utils/Counters.sol"; + import "@openzeppelin/contracts/access/Ownable.sol"; + import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; + + contract MyNFT is ERC721URIStorage, Ownable { + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + + constructor() ERC721("MyNFT", "NFT") {} + + function mintNFT(address recipient, string memory tokenURI) + public onlyOwner + returns (uint256) + { + _tokenIds.increment(); + + uint256 newItemId = _tokenIds.current(); + _mint(recipient, newItemId); + _setTokenURI(newItemId, tokenURI); + + return newItemId; + } + } + ``` + +3. 因為我們繼承了 OpenZeppelin 合約函式庫的類別,所以請在命令列中執行 `npm install @openzeppelin/contracts^4.0.0`,將函式庫安裝至我們的資料夾中。 + +那麼,這段程式碼的確切功用是什麼? 讓我們拆解他,一行一行解說。 + +在智慧合約的頂部,我們匯入了三個 [OpenZeppelin](https://openzeppelin.com/) 智慧合約類別: + +- @openzeppelin/contracts/token/ERC721/ERC721.sol 包含了ERC-721的執行標準,我們的智慧型合約將會繼承他。 要成為一個有效的非同質化代幣 (NFT),你的智慧型合約必須執行所有在ERR-721標準裡的方法。 若要深入了解繼承的 ERC-721 函式,請參閱[此處](https://eips.ethereum.org/EIPS/eip-721)的介面定義。 + +- @openzeppelin/contracts/utils/Counters.sol 提供一個儘可以逐一遞減或遞增的計數器。 我們的智慧型合約使用一個計數器去持續追蹤所有NFT的鑄造數與設定一個唯一的ID在每一個新NFT。 (每一個被鑄造的NFT都要用一個智慧型合約分配一個唯一的ID -- 我們的ID在這裡只有使用NFT總數來決定。 舉一個例子,我們鑄造的第一個NFT擁有一個「1」的ID,第二個則是「2」,依此類推。) + +- @openzeppelin/contracts/access/Ownable.sol 會在我們的智慧合約上設定[存取權控制](https://docs.openzeppelin.com/contracts/3.x/access-control),如此一來,只有智慧合約的擁有者 (也就是您) 可以鑄造 NFT。 (編註: 包括使用權控制指示一種偏好。 如果你不喜歡有人可以使用你的智慧型合約鑄造NFT,移除在第十行的的"Ownable",以及第17行的"onlyOwner"。) + +在引入以上函式庫後,我們有我們傳統的NFT智慧型合約,程式碼意外的短,他只包刮一個計數器,一個構建函數,和一個函式! 這要歸功於我們繼承的 OpenZeppelin 合約,它實作了我們建立 NFT 所需的大部分方法,例如傳回 NFT 擁有者的 `ownerOf`,以及將 NFT 擁有權從一個帳戶轉移至另一個帳戶的 `transferFrom`。 + +在我們的 ERC-721 的建構函數裡(constractor),你可能會注意到我們傳入了兩個字串,"MyNFT" 和 "NFT"。 第一個變數是智慧型合約名稱,第二個則是他的代號(象徵 symbol)。 你可以為每一個變數命名。 + +最後,我們有了函式 `mintNFT(address recipient, string memory tokenURI)`,可以用來鑄造 NFT! 你可能會注意到這個函數傳入了兩個變數: + +- `address recipient` 會指定接收您剛鑄造好的 NFT 的地址 + +- `string memory tokenURI` 是一個字串,應解析為描述 NFT 元資料的 JSON 文件。 NFT的後設數據(metadata) 實際上是使其生存的原因,允許它具有可配置的屬性,例如名稱,描述,圖像和其他屬性。 在這個教學的第二部份我們將會解釋如何設定這個後設資料(metadata)。 + +`mintNFT` 會從繼承的 ERC-721 函式庫呼叫一些方法,並最終傳回一個數字,此數字代表新鑄造的 NFT 的 ID。 + +## 第 11 步:將 MetaMask 和 Alchemy 連線至您的專案 {#connect-metamask-and-alchemy} + +現在,我們已經創建了一個MetaMask錢包、Alchemy帳戶,並編寫了我們的智慧型合約,是時候將這三者連接起來了。 + +每一個從你的虛擬錢包送出的交易都需要用你的私鑰簽名。 為了給予程式這個權限,我們可以把私鑰(還有 Alchemy API key)存在環境檔案中。 + +若要深入了解傳送交易,請參閱這篇關於使用 web3 傳送交易的[教學文章](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)。 + +首先,安裝 dotenv 套件。 + + ``` + npm install dotenv --save + ``` + +然後,在我們專案的根目錄中建立一個 `.env` 檔案,並在其中新增您的 MetaMask 私鑰和 HTTP Alchemy API URL。 + +- 請遵循[這些指示](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key)從 MetaMask 匯出您的私鑰 + +- 請參見下麵獲取HTTP Alchemy 接口地址並將其複製到剪貼板 + +![複製您的 Alchemy API URL](./copy-alchemy-api-url.gif) + +您的 `.env` 檔案現在應該會像這樣: + + ``` + API_URL="https://eth-sepolia.g.alchemy.com/v2/your-api-key" + PRIVATE_KEY="your-metamask-private-key" + ``` + +為了將這些變數實際連線至我們的程式碼,我們會在第 13 步的 hardhat.config.js 檔案中參考這些變數。 + + + +## 第 12 步:安裝 Ethers.js {#install-ethers} + +Ethers.js 是一個函式庫,它將[標準 JSON-RPC 方法](/developers/docs/apis/json-rpc/)包裝成更方便使用者使用的方法,讓與以太坊互動和發出請求變得更簡單。 + +Hardhat 讓整合[外掛程式](https://hardhat.org/plugins/)以取得額外工具和擴充功能變得超級簡單。 我們將利用 [Ethers plugin](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers) 進行合約部署 ([Ethers.js](https://github.com/ethers-io/ethers.js/) 有一些非常簡潔的合約部署方法)。 + +在你的專案目錄輸入: + + ``` + npm install --save-dev @nomiclabs/hardhat-ethers ethers@^5.0.0 + ``` + +我們會在下一步 hardhat.config.js 將 ethers 納入進來。 + +## 第 13 步:更新 hardhat.config.js {#update-hardhat-config} + +我們目前已經新增了幾個套件,現在則是要更新 hardhat.config.js ,告訴專案我們要用它們。 + +將 hardhat.config.js 更新成如下方: + +```js + /** + * @type import('hardhat/config').HardhatUserConfig + */ + require('dotenv').config(); + require("@nomiclabs/hardhat-ethers"); + const { API_URL, PRIVATE_KEY } = process.env; + module.exports = { + solidity: "0.8.1", + defaultNetwork: "sepolia", + networks: { + hardhat: {}, + sepolia: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`] + } + }, + } +``` + +## 第 14 步:編譯我們的合約 {#compile-contract} + +為了確認一切運作正常,我們來編譯合約。 編譯任務是安全帽的內部任務之一 + +在命令列工具輸入: + + ``` + npx hardhat compile + ``` + +你可能會看到關於“源文件中未提供SPDX許可證識別碼”的警告,但是不用擔心,希望其他的看起來都正常 如果沒有,您隨時可以在 [Alchemy discord](https://discord.gg/u72VCg3) 中傳送訊息。 + +## 第 15 步:撰寫我們的部署腳本 {#write-deploy} + +現在我們已經寫好了合約,並且也搞定配置檔案。現在我們該來撰寫部署合約的腳本。 + +前往 `scripts/` 資料夾並建立一個名為 `deploy.js` 的新檔案,在其中加入以下內容: + +```js +async function main() { + const MyNFT = await ethers.getContractFactory("MyNFT") + + // 開始部署,傳回一個解析為合約物件的 promise + const myNFT = await MyNFT.deploy() + await myNFT.deployed() + console.log("Contract deployed to address:", myNFT.address) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +``` + +Hardhat 在其[合約教學文章](https://hardhat.org/tutorial/testing-contracts.html#writing-tests)中詳細地解釋了每一行程式碼的作用,我們在此採用了他們的解釋。 + + ``` + const MyNFT = await ethers.getContractFactory("MyNFT"); + ``` + +Ethers.js 中的 ContractFactory 是用於部署新智慧型合約的抽象對象。所以這裡的MyNFT是我們NFT合約實例的工廠 使用 hardhat-ethers 插件时,ContractFactory 和合約實例默認與第一個簽名帳戶相連。 + + ``` + const myNFT = await MyNFT.deploy(); + ``` + +調用 ContractFactory 程式碼中的 deploy() 函數會啟動合約部署,然後返回解析為合約的Promise。 這就是和我們的智慧型合約函數有一對一的方法的物件。 + +## 第 16 步:部署我們的合約 {#deploy-contract} + +我們終於準備好要部署合約了! 返回你專案目錄的根目錄,在命令行中於行: + + ``` + npx hardhat --network sepolia run scripts/deploy.js + ``` + +你會看到像這樣的輸出: + + ``` + 合約已部署至地址:0x4C5266cCc4b3F426965d2f51b6D910325a0E7650 + ``` + +如果我們前往 [Sepolia etherscan](https://sepolia.etherscan.io/) 並搜尋我們的合約地址,我們應該能夠看到它已成功部署。 如果你沒立即看到它,請稍等片刻,因為它可能需要一些時間。 這個交易執行看起來會像這樣: + +![在 Etherscan 上檢視您的交易地址](./etherscan-sepoila-contract-creation.png) + +「From」地址應與您的 MetaMask 帳戶地址相符,而「To」地址將顯示「Contract Creation」。 如果我們電機進入交易,我們將在“To”字段中看到我們的合約地址: + +![在 Etherscan 上檢視您的合約地址](./etherscan-sepolia-tx-details.png) + +太棒了! 您剛剛已將您的 NFT 智慧合約部署到以太坊 (測試網) 鏈上了! + +為了了解幕後情況,讓我們前往 [Alchemy 儀表板](https://dashboard.alchemyapi.io/explorer)中的「Explorer」分頁。 如果你有多個Alchemy應用程序,請確保按應用程序篩選,然後選擇“MyNFT”。 + +![使用 Alchemy 的 Explorer 儀表板檢視「幕後」進行的呼叫](./alchemy-explorer-goerli.png) + +在這裡你會看到Hardhat/Ethers 替我們在後端完成的一系列JSON-RPC調用,當我們調用.deploy() 函數時候。 這裡要特別提出兩個重要的呼叫:[eth_sendRawTransaction](/developers/docs/apis/json-rpc/#eth_sendrawtransaction) 是將我們的智慧合約實際寫入 Sepolia 鏈的請求;[eth_getTransactionByHash](/developers/docs/apis/json-rpc/#eth_gettransactionbyhash) 則是在給定哈希的情況下讀取交易資訊的請求 (這是傳送交易時的典型模式)。 若要深入了解傳送交易,請參閱這篇關於[使用 Web3 傳送交易](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)的教學文章。 + +以上即為這個教程的第1部分全部內容。 在[第 2 部分,我們將透過鑄造 NFT 來實際與我們的智慧合約互動](/developers/tutorials/how-to-mint-an-nft/),而在[第 3 部分,我們將示範如何在您的以太坊錢包中檢視您的 NFT](/developers/tutorials/how-to-view-nft-in-metamask/)! diff --git a/public/content/translations/zh-tw/developers/tutorials/interact-with-other-contracts-from-solidity/index.md b/public/content/translations/zh-tw/developers/tutorials/interact-with-other-contracts-from-solidity/index.md new file mode 100644 index 00000000000..a89d525c66d --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/interact-with-other-contracts-from-solidity/index.md @@ -0,0 +1,172 @@ +--- +title: "從 Solidity 與其他合約互動" +description: "如何從現有合約部署智能合約並與其互動" +author: "jdourlens" +tags: [ "smart contracts", "solidity", "remix", "deploying", "composability" ] +skill: advanced +lang: zh-tw +published: 2020-04-05 +source: EthereumDev +sourceUrl: https://ethereumdev.io/interact-with-other-contracts-from-solidity/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +在先前的教學中,我們學習了許多知識,像是[如何部署您的第一個智能合約](/developers/tutorials/deploying-your-first-smart-contract/),以及為它新增一些功能,例如[使用修飾詞控制存取](https://ethereumdev.io/organize-your-code-and-control-access-to-your-smart-contract-with-modifiers/)或[在 Solidity 中處理錯誤](https://ethereumdev.io/handle-errors-in-solidity-with-require-and-revert/)。 在本教學中,我們將學習如何從現有合約部署智能合約並與其互動。 + +我們將建立一個名為 `CounterFactory` 的合約工廠,讓任何人都能夠透過它來建立自己的 `Counter` 智能合約。 首先,這是我們初始 `Counter` 智能合約的程式碼: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + uint256 private _count; + address private _owner; + address private _factory; + + + modifier onlyOwner(address caller) { + require(caller == _owner, "您不是此合約的擁有者"); + _; + } + + modifier onlyFactory() { + require(msg.sender == _factory, "您需要使用工廠"); + _; + } + + constructor(address owner) public { + _owner = owner; + _factory = msg.sender; + } + + function getCount() public view returns (uint256) { + return _count; + } + + function increment(address caller) public onlyFactory onlyOwner(caller) { + _count++; + } + +} +``` + +請注意,我們稍微修改了合約程式碼,以便追蹤工廠的地址和合約擁有者的地址。 當您從另一個合約呼叫某合約的程式碼時,`msg.sender` 將會是我們合約工廠的地址。 這是**一個非常重要的理解要點**,因為使用合約與其他合約互動是常見的做法。 因此,在複雜情況下,您應該要留意誰是發送者。 + +為此,我們也新增了 `onlyFactory` 修飾詞,它能確保改變狀態的函式只能由工廠呼叫,工廠會將原始呼叫者當作參數傳遞。 + +在我們新的 `CounterFactory`(它會管理所有其他的 Counter)內部,我們會新增一個 mapping,將擁有者與其計數器合約的地址關聯起來: + +```solidity +mapping(address => Counter) _counters; +``` + +在以太坊中,映射 (mapping) 相當於 javascript 中的物件,可以將 A 型別的鍵對應到 B 型別的值。在此案例中,我們將擁有者的地址與其 Counter 的實例對應起來。 + +為某人實例化一個新的 Counter,看起來會像這樣: + +```solidity + function createCounter() public { + require (_counters[msg.sender] == Counter(0)); + _counters[msg.sender] = new Counter(msg.sender); + } +``` + +我們首先檢查該使用者是否已擁有一個計數器。 如果該使用者沒有計數器,我們會將其地址傳遞給 `Counter` 建構函式以實例化一個新的計數器,並將新建立的實例指派給 mapping。 + +要取得特定 Counter 的計數,會像這樣: + +```solidity +function getCount(address account) public view returns (uint256) { + require (_counters[account] != Counter(0)); + return (_counters[account].getCount()); +} + +function getMyCount() public view returns (uint256) { + return (getCount(msg.sender)); +} +``` + +第一個函式會檢查指定地址是否存在 Counter 合約,然後從實例中呼叫 `getCount` 方法。 第二個函式 `getMyCount` 只是個簡潔的作法,直接將 `msg.sender` 傳遞給 `getCount` 函式。 + +`increment` 函式非常相似,但是它將原始交易的發送者傳遞給 `Counter` 合約: + +```solidity +function increment() public { + require (_counters[msg.sender] != Counter(0)); + Counter(_counters[msg.sender]).increment(msg.sender); + } +``` + +請注意,如果呼叫太多次,我們的計數器可能會發生溢出。 您應該盡可能使用 [SafeMath 函式庫](https://ethereumdev.io/using-safe-math-library-to-prevent-from-overflows/),以防止這種可能的情況發生。 + +要部署我們的合約,您將需要提供 `CounterFactory` 和 `Counter` 兩者的程式碼。 例如在 Remix 中部署時,您需要選擇 CounterFactory。 + +這是完整的程式碼: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + uint256 private _count; + address private _owner; + address private _factory; + + + modifier onlyOwner(address caller) { + require(caller == _owner, "您不是此合約的擁有者"); + _; + } + + modifier onlyFactory() { + require(msg.sender == _factory, "您需要使用工廠"); + _; + } + + constructor(address owner) public { + _owner = owner; + _factory = msg.sender; + } + + function getCount() public view returns (uint256) { + return _count; + } + + function increment(address caller) public onlyFactory onlyOwner(caller) { + _count++; + } + +} + +contract CounterFactory { + + mapping(address => Counter) _counters; + + function createCounter() public { + require (_counters[msg.sender] == Counter(0)); + _counters[msg.sender] = new Counter(msg.sender); + } + + function increment() public { + require (_counters[msg.sender] != Counter(0)); + Counter(_counters[msg.sender]).increment(msg.sender); + } + + function getCount(address account) public view returns (uint256) { + require (_counters[account] != Counter(0)); + return (_counters[account].getCount()); + } + + function getMyCount() public view returns (uint256) { + return (getCount(msg.sender)); + } + +} +``` + +編譯後,在 Remix 的部署區塊中,您將會選擇要部署的工廠: + +![在 Remix 中選擇要部署的工廠](./counterfactory-deploy.png) + +然後您可以操作您的合約工廠,並檢查值的變化。 如果您想從不同的地址呼叫智能合約,您需要在 Remix 的帳戶 (Account) 選項中變更地址。 diff --git a/public/content/translations/zh-tw/developers/tutorials/ipfs-decentralized-ui/index.md b/public/content/translations/zh-tw/developers/tutorials/ipfs-decentralized-ui/index.md new file mode 100644 index 00000000000..54731d85d34 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/ipfs-decentralized-ui/index.md @@ -0,0 +1,73 @@ +--- +title: "用於去中心化使用者介面的 IPFS" +description: "本使用教學會教讀者如何使用 IPFS 來儲存去中心化應用程式的使用者介面。 儘管應用程式的資料和商業邏輯是去中心化的,但如果沒有抗審查的使用者介面,使用者還是可能會失去存取權限。" +author: Ori Pomerantz +tags: [ "ipfs" ] +skill: beginner +lang: zh-tw +published: 2024-06-29 +--- + +您寫了一個很棒的全新去中心化應用程式。 您甚至還為它寫了一個[使用者介面](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/)。 但現在您擔心有人會透過讓您的使用者介面下線來審查它,而它只是雲端上的一台伺服器。 在本使用教學中,您會學習如何將使用者介面放到**[星際檔案系統 (IPFS)](https://ipfs.tech/developers/)** 上以避免審查,如此一來任何感興趣的人都能將它釘選在伺服器上供未來存取。 + +您可以使用第三方服務 (例如 [Fleek](https://resources.fleek.xyz/docs/)) 來完成所有工作。 本使用教學適合想要深入了解自己在做什麼的人,即使這代表更多的工作。 + +## 從本機開始 {#getting-started-locally} + +有多個[第三方 IPFS 供應商](https://docs.ipfs.tech/how-to/work-with-pinning-services/#use-a-third-party-pinning-service),但最好從在本機執行 IPFS 開始測試。 + +1. 安裝 [IPFS 使用者介面](https://docs.ipfs.tech/install/ipfs-desktop/#install-instructions)。 + +2. 建立一個包含您網站的目錄。 如果您正在使用 [Vite](https://vite.dev/),請使用此命令: + + ```sh + pnpm vite build + ``` + +3. 在 IPFS Desktop 中,按一下 **Import > Folder**,然後選擇您在上一步驟中建立的目錄。 + +4. 選擇您剛上傳的資料夾,然後按一下 **Rename**。 為它取一個更有意義的名稱。 + +5. 再次選取它,然後按一下 **Share link**。 將 URL 複製到剪貼簿。 連結會與 `https://ipfs.io/ipfs/QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ` 相似。 + +6. 按一下 **Status**。 展開 **Advanced** 索引標籤以查看閘道器地址。 例如,在我的系統上,地址是 `http://127.0.0.1:8080`。 + +7. 將連結步驟中的路徑與閘道器地址組合起來,即可找到您的地址。 例如,對於上述範例,URL 為 `http://127.0.0.1:8080/ipfs/QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ`。 在瀏覽器中開啟該 URL 以查看您的網站。 + +## 上傳 {#uploading} + +現在您可以使用 IPFS 在本機提供檔案服務,這不是非常令人興奮。 下一步是在您離線時,讓全世界都能使用這些檔案。 + +有許多知名的[釘選服務](https://docs.ipfs.tech/concepts/persistence/#pinning-services)。 選擇其中一個。 無論您使用哪種服務,都需要建立一個帳戶,並在 IPFS desktop 中提供**內容識別碼 (CID)**。 + +就我個人而言,我發現 [4EVERLAND](https://docs.4everland.org/storage/4ever-pin/guides) 是最容易使用的。 以下是其使用說明: + +1. 瀏覽至[儀表板](https://dashboard.4everland.org/overview)並使用您的錢包登入。 + +2. 在左側邊欄中,按一下 **Storage > 4EVER Pin**。 + +3. 按一下 **Upload > Selected CID**。 為您的內容命名,並提供 IPFS desktop 的 CID。 目前 CID 是一個以 `Qm` 開頭的字串,後面跟著 44 個字母和數字,代表一個 [base-58 編碼](https://medium.com/bootdotdev/base64-vs-base58-encoding-c25553ff4524)的哈希,例如 `QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ`,但[這很可能會改變](https://docs.ipfs.tech/concepts/content-addressing/#version-1-v1)。 + +4. 初始狀態為 **Queued**。 重新載入,直到狀態變為 **Pinned**。 + +5. 按一下您的 CID 以取得連結。 您可以在[這裡](https://bafybeifqka2odrne5b6l5guthqvbxu4pujko2i6rx2zslvr3qxs6u5o7im/)看到我的應用程式。 + +6. 您可能需要啟用您的帳戶,才能將其釘選超過一個月。 啟用帳戶的費用約為 1 美元。 如果您關閉了它,請登出再重新登入,系統會再次要求您啟用。 + +## 從 IPFS 使用 {#using-from-ipfs} + +此時,您已擁有一個指向中心化閘道器的連結,該閘道器為您的 IPFS 內容提供服務。 簡言之,您的使用者介面可能更安全一些,但它仍然不是抗審查的。 要實現真正的抗審查,使用者需要[直接從瀏覽器](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites)使用 IPFS。 + +一旦您安裝了它 (並且桌面版 IPFS 正常運作),您就可以在任何網站上前往 [/ipfs/``](https://any.site/ipfs/bafybeifqka2odrne5b6l5guthqvbxu4pujko2i6rx2zslvr3qxs6u5o7im),您將以去中心化的方式獲得該內容。 + +## 缺點 {#drawbacks} + +您無法可靠地刪除 IPFS 檔案,因此只要您在修改使用者介面,最好還是將其保持中心化,或使用[星際名稱系統 (IPNS)](https://docs.ipfs.tech/concepts/ipns/#mutability-in-ipfs),這是一個在 IPFS 之上提供可變性的系統。 當然,任何可變的東西都可以被審查,在 IPNS 的情況下,可以透過向擁有其對應私密金鑰的人施壓來達成。 + +此外,某些套件在 IPFS 上會出現問題,因此如果您的網站非常複雜,這可能不是一個好的解決方案。 當然,任何依賴伺服器整合的東西都無法僅僅透過將用戶端放在 IPFS 上來去中心化。 + +## 結論 {#conclusion} + +就像以太坊讓您能夠將去中心化應用程式的資料庫和商業邏輯層面去中心化一樣,IPFS 也能讓您將使用者介面去中心化。 這能讓您阻斷針對您的去中心化應用程式的另一個攻擊媒介。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md b/public/content/translations/zh-tw/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md new file mode 100644 index 00000000000..598574bf3fd --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md @@ -0,0 +1,104 @@ +--- +title: "使用 create-eth-app 快速啟動您的去中心化應用程式前端開發" +description: "create-eth-app 使用方法及其功能概覽" +author: "Markus Waas" +tags: [ "frontend", "javascript", "ethers.js", "the graph", "defi" ] +skill: beginner +lang: zh-tw +published: 2020-04-27 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/create-eth-app +--- + +上次我們看過了 [Solidity 的整體概況](https://soliditydeveloper.com/solidity-overview-2020),並且已經提到了 [create-eth-app](https://github.com/PaulRBerg/create-eth-app)。 現在您將了解如何使用它、整合了哪些功能,以及關於如何擴展它的額外想法。 此應用程式由 [Sablier](http://sablier.com/) 的創辦人 Paul Razvan Berg 發起,它能快速啟動您的前端開發,並提供數個可選的整合選項。 + +## 安裝 {#installation} + +安裝需要 Yarn 0.25 或更高版本 (`npm install yarn --global`)。 執行方法很簡單: + +```bash +yarn create eth-app my-eth-app +cd my-eth-app +yarn react-app:start +``` + +它在底層使用 [create-react-app](https://github.com/facebook/create-react-app)。 若要檢視您的應用程式,請開啟 `http://localhost:3000/`。 當您準備好部署到生產環境時,請使用 yarn build 建立一個最小化的套件。 一個簡單的託管方法是使用 [Netlify](https://www.netlify.com/)。 您可以建立一個 GitHub repo,將其新增至 Netlify,設定建置指令,就完成了! 您的應用程式將會被託管,且所有人都能使用。 而且完全免費。 + +## 功能 {#features} + +### React & create-react-app {#react--create-react-app} + +首先是此應用程式的核心:React 以及 _create-react-app_ 附帶的所有額外功能。 如果您不想整合以太坊,只使用這個也是個絕佳的選擇。 [React](https://react.dev/) 本身讓建置互動式使用者介面 (UI) 變得非常簡單。 它可能不像 [Vue](https://vuejs.org/) 那樣對初學者友善,但它仍然是主流,擁有更多功能,而且最重要的是有數以千計的額外函式庫可供選擇。 _create-react-app_ 同樣讓入門變得非常簡單,它包含: + +- 支援 React、JSX、ES6、TypeScript 和 Flow 語法。 +- ES6 以外的額外語言功能,例如物件展開運算子。 +- 自動加上前綴的 CSS,所以您不需要 -webkit- 或其他前綴。 +- 一個快速的互動式單元測試執行器,內建支援覆蓋率報告。 +- 一個即時開發伺服器,會對常見錯誤發出警告。 +- 一個建置腳本,可將 JS、CSS 和圖片打包用於生產,並附有哈希和來源對應檔。 + +特別是 _create-eth-app_ 利用了新的 [Hook 特效](https://legacy.reactjs.org/docs/hooks-effect.html)。 這是一種撰寫功能強大但體積極小的所謂「函式元件」的方法。 請參閱下文關於 Apollo 的章節,以了解它們在 _create-eth-app_ 中的使用方式。 + +### Yarn Workspaces {#yarn-workspaces} + +[Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) 讓您擁有多個套件,但能夠從根目錄管理所有套件,並使用 `yarn install` 一次為所有套件安裝依賴項。 這對於像智慧合約地址/ABI 管理 (關於您部署了哪些智慧合約以及如何與它們通訊的資訊) 或 The Graph 整合等較小的附加套件特別有意義,這兩者都是 `create-eth-app` 的一部分。 + +### ethers.js {#ethersjs} + +雖然 [Web3](https://docs.web3js.org/) 仍是主流,但 [ethers.js](https://docs.ethers.io/) 在去年作為替代方案獲得了更多關注,並且是整合到 _create-eth-app_ 中的函式庫。 您可以使用這個函式庫,也可以將其更換為 Web3,或考慮升級到幾乎脫離測試版的 [ethers.js v5](https://docs.ethers.org/v5/)。 + +### The Graph {#the-graph} + +與 [RESTful API](https://restfulapi.net/) 相比,[GraphQL](https://graphql.org/) 是另一種處理資料的方式。 與 RESTful API 相比,它們有幾個優勢,特別是對於去中心化區塊鏈資料而言。 如果您對這背後的原因感興趣,可以看看 [GraphQL Will Power the Decentralized Web](https://medium.com/graphprotocol/graphql-will-power-the-decentralized-web-d7443a69c69a)。 + +通常,您會直接從您的智慧合約中獲取資料。 想要讀取最新一筆交易的時間嗎? 只需呼叫 `MyContract.methods.latestTradeTime().call()`,它會從一個以太坊節點將資料擷取到您的去中心化應用程式中。 但如果您需要數百個不同的資料點呢? 這將導致對節點進行數百次的資料擷取,每次都需要一次 [往返時延 (RTT)](https://wikipedia.org/wiki/Round-trip_delay_time),從而使您的去中心化應用程式變得緩慢且效率低下。 一個變通方法可能是在您的合約內部設置一個擷取器呼叫函式,一次傳回多筆資料。 不過,這並非總是理想的解決方案。 + +此外,您可能也會對歷史資料感興趣。 您不僅想知道最後一次交易的時間,還想知道您自己進行過的所有交易的時間。 使用 _create-eth-app_ 的子圖套件,閱讀 [文件](https://thegraph.com/docs/en/subgraphs/developing/creating/starting-your-subgraph) 並將其應用於您自己的合約。 如果您正在尋找熱門的智慧合約,甚至可能已經有現成的子圖了。 查看 [子圖瀏覽器](https://thegraph.com/explorer/)。 + +一旦您有了子圖,您就可以在您的去中心化應用程式中編寫一個簡單的查詢,以擷取所有您需要的重要的區塊鏈資料,包括歷史資料,而只需要一次擷取。 + +### Apollo {#apollo} + +多虧了 [Apollo Boost](https://www.apollographql.com/docs/react/get-started/) 整合,您可以輕鬆地將 The Graph 整合到您的 React 去中心化應用程式中。 特別是當使用 [React Hook 和 Apollo](https://www.apollographql.com/blog/apollo-client-now-with-react-hooks) 時,擷取資料就像在您的元件中編寫單一 GraphQL 查詢一樣簡單: + +```js +const { loading, error, data } = useQuery(myGraphQlQuery) + +React.useEffect(() => { + if (!loading && !error && data) { + console.log({ data }) + } +}, [loading, error, data]) +``` + +## 範本 {#templates} + +此外,您可以從數個不同的範本中進行選擇。 目前,您可以使用 Aave、Compound、UniSwap 或 Sablier 整合。 它們都添加了重要的服務智慧合約地址以及預製的子圖整合。 只需將範本添加到創建指令中即可,例如 `yarn create eth-app my-eth-app --with-template aave`。 + +### Aave {#aave} + +[Aave](https://aave.com/) 是一個去中心化的貨幣借貸市場。 存款人向市場提供流動性以賺取被動收入,而借款人則能夠使用抵押品進行借款。 Aave 的一個獨特功能是 [閃電貸](https://aave.com/docs/developers/flash-loans),它允許您在沒有任何抵押品的情況下借款,只要您在單一交易內歸還貸款即可。 這在某些情況下可能很有用,例如在進行套利交易時為您提供額外現金。 + +可以賺取利息的交易代幣稱為 _aTokens_。 + +當您選擇將 Aave 與 _create-eth-app_ 整合時,您將獲得一個 [子圖整合](https://docs.aave.com/developers/getting-started/using-graphql)。 Aave 使用 The Graph,並已在 [Ropsten](https://thegraph.com/explorer/subgraph/aave/protocol-ropsten) 和 [主網](https://thegraph.com/explorer/subgraph/aave/protocol)上以 [原始](https://thegraph.com/explorer/subgraph/aave/protocol-raw) 或 [格式化](https://thegraph.com/explorer/subgraph/aave/protocol) 形式為您提供了數個即用型子圖。 + +![Aave 閃電貸迷因 – "是啊,如果我的閃電貸能維持超過 1 筆交易,那就太棒了"](./flashloan-meme.png) + +### Compound {#compound} + +[Compound](https://compound.finance/) 與 Aave 類似。 該整合已經包含了新的 [Compound v2 子圖](https://medium.com/graphprotocol/https-medium-com-graphprotocol-compound-v2-subgraph-highlight-a5f38f094195)。 可想而知,這裡賺取利息的代幣稱為 _cTokens_。 + +### Uniswap {#uniswap} + +[Uniswap](https://uniswap.exchange/) 是一個去中心化交易所 (DEX)。 流動性提供者可以透過為交易雙方提供所需的代幣或以太幣來賺取費用。 它被廣泛使用,因此在眾多代幣中擁有最高的流動性之一。 您可以輕鬆地將其整合到您的去中心化應用程式中,例如,允許使用者將他們的 ETH 兌換成 DAI。 + +可惜的是,在撰寫本文時,該整合僅適用於 Uniswap v1,而不適用於 [剛發布的 v2](https://uniswap.org/blog/uniswap-v2/)。 + +### Sablier {#sablier} + +[Sablier](https://sablier.com/) 允許使用者進行串流式金錢支付。 在初始設定後,您實際上可以持續收到款項,而無需進一步管理,而不是一次性的發薪日。 該整合包含了它 [自己的子圖](https://thegraph.com/explorer/subgraph/sablierhq/sablier)。 + +## 下一步? {#whats-next} + +如果您對 _create-eth-app_ 有任何疑問,請前往 [Sablier 社群伺服器](https://discord.gg/bsS8T47),您可以在那裡與 _create-eth-app_ 的作者聯繫。 作為接下來的初步步驟,您可能需要整合一個 UI 框架,如 [Material UI](https://mui.com/material-ui/),為您實際需要的資料編寫 GraphQL 查詢,並設定部署。 diff --git a/public/content/translations/zh-tw/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md b/public/content/translations/zh-tw/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md new file mode 100644 index 00000000000..866f070d7b8 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md @@ -0,0 +1,269 @@ +--- +title: "使用 SQL 學習基礎以太坊主題" +description: "本教學課程將透過結構化查詢語言 (SQL) 查詢鏈上資料,協助讀者了解基本的以太坊概念,包括交易、區塊和 Gas。" +author: "Paul Apivat" +tags: [ "SQL", "Querying", "Transactions" ] +skill: beginner +lang: zh-tw +published: 2021-05-11 +source: paulapivat.com +sourceUrl: https://paulapivat.com/post/query_ethereum/ +--- + +許多以太坊教學課程都以開發人員為對象,但卻缺乏為資料分析師,或是想在不執行用戶端或節點的情況下查看鏈上資料的人們所準備的教育資源。 + +本教學課程透過 [Dune Analytics](https://dune.com/) 提供的介面,以結構化查詢語言 (SQL) 查詢鏈上資料,協助讀者了解基本的以太坊概念,包括交易、區塊和 Gas。 + +鏈上資料可以幫助我們了解以太坊、網路以及作為運算能力的經濟體,並且應該作為基礎,以了解以太坊現今面臨的挑戰 (例如 Gas 費用上漲),更重要的是,可以圍繞擴展解決方案進行討論。 + +### 交易 {#transactions} + +使用者在以太坊的旅程,始於初始化一個由使用者控制的帳戶,或一個具有 ETH 餘額的實體。 帳戶有兩種類型:由使用者控制的帳戶,或智能合約帳戶 (請參閱 [ethereum.org](/developers/docs/accounts/))。 + +任何帳戶都可以在 [Etherscan](https://etherscan.io/) 或 [Blockscout](https://eth.blockscout.com/) 等區塊瀏覽器上查看。 區塊瀏覽器是以太坊資料的入口網站。 它們會即時顯示區塊、交易、礦工、帳戶和其他鏈上活動的資料 (請參閱[此處](/developers/docs/data-and-analytics/block-explorers/))。 + +然而,使用者可能希望直接查詢資料,以核對外部區塊瀏覽器提供的資訊。 [Dune Analytics](https://dune.com/) 為任何具備 SQL 知識的人提供此功能。 + +以供參考,以太坊基金會 (EF) 的智能合約帳戶可以在 [Blockscout](https://eth.blockscout.com/address/0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe) 上查看。 + +需要注意的是,所有帳戶,包括以太坊基金會的帳戶,都有一個可用於傳送和接收交易的公開地址。 + +Etherscan 上的帳戶餘額包含一般交易和內部交易。 內部交易,雖然名為內部交易,但並非會改變鏈狀態的「實際」交易。 它們是透過執行合約來啟動的價值轉移 ([來源](https://ethereum.stackexchange.com/questions/3417/how-to-get-contract-internal-transactions))。 由於內部交易沒有簽名,所以「不會」被包含在區塊鏈上,也無法使用 Dune Analytics 查詢。 + +因此,本教學課程將著重於一般交易。 查詢方式如下: + +```sql +WITH temp_table AS ( +SELECT + hash, + block_number, + block_time, + "from", + "to", + value / 1e18 AS ether, + gas_used, + gas_price / 1e9 AS gas_price_gwei +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +) +SELECT + hash, + block_number, + block_time, + "from", + "to", + ether, + (gas_used * gas_price_gwei) / 1e9 AS txn_fee +FROM temp_table +``` + +這將會產生與 Etherscan 交易頁面上所提供的相同資訊。 為了比較,以下是兩個來源: + +#### Etherscan {#etherscan} + +![](./etherscan_view.png) + +[Blockscout 上的以太坊基金會合約頁面。](https://eth.blockscout.com/address/0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe) + +#### Dune Analytics {#dune-analytics} + +![](./dune_view.png) + +你可以在[此處](https://dune.com/paulapivat/Learn-Ethereum)找到儀表板。 按一下表格以查看查詢 (也請參閱上方)。 + +### 交易詳解 {#breaking_down_transactions} + +已提交的交易包含幾項資訊,其中包括 ([來源](/developers/docs/transactions/)): + +- **收款人**:接收地址 (查詢為 "to") +- **簽名**:雖然寄件人的私鑰會簽署交易,但我們可以使用 SQL 查詢的是寄件人的公開地址 ("from")。 +- **價值**:這是轉移的 ETH 金額 (請參閱 `ether` 欄位)。 +- **資料**:這是經過哈希運算的任意資料 (請參閱 `data` 欄位) +- **gasLimit** – 交易可耗用的 gas 單位上限。 gas 單位代表運算步驟 +- **maxPriorityFeePerGas** - 作為給礦工的小費,每單位 gas 的優先費用上限 +- **maxFeePerGas** - 願意為交易支付的每單位 gas 費用上限 (包含 baseFeePerGas 和 maxPriorityFeePerGas) + +我們可以查詢傳送至以太坊基金會公開地址的交易的特定資訊: + +```sql +SELECT + "to", + "from", + value / 1e18 AS ether, + data, + gas_limit, + gas_price / 1e9 AS gas_price_gwei, + gas_used, + ROUND(((gas_used / gas_limit) * 100),2) AS gas_used_pct +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +``` + +### 區塊 {#blocks} + +每筆交易都會改變以太坊虛擬機 ([EVM](/developers/docs/evm/)) 的狀態 ([來源](/developers/docs/transactions/))。 交易會廣播至網路進行驗證,並包含在區塊中。 每筆交易都與一個區塊編號相關聯。 若要查看資料,我們可以查詢特定的區塊編號:12396854 (截至本文撰寫時 [2021/5/11],此為以太坊基金會交易中最新的區塊)。 + +此外,當我們查詢接下來的兩個區塊時,我們可以看到每個區塊都包含前一個區塊的哈希 (即父哈希),這說明了區塊鏈是如何形成的。 + +每個區塊都包含對其父區塊的引用。 如下方 `hash` 和 `parent_hash` 欄位之間所示 ([來源](/developers/docs/blocks/)): + +![parent_hash](./parent_hash.png) + +這是 Dune Analytics 上的[查詢](https://dune.com/queries/44856/88292): + +```sql +SELECT + time, + number, + hash, + parent_hash, + nonce +FROM ethereum."blocks" +WHERE "number" = 12396854 OR "number" = 12396855 OR "number" = 12396856 +LIMIT 10 +``` + +我們可以透過查詢時間、區塊編號、難度、哈希、父哈希和 nonce 來檢視一個區塊。 + +此查詢唯一未涵蓋的是_交易列表_和_狀態根_,這需要下方另一個獨立的查詢。 完整或封存節點將儲存所有交易和狀態轉換,讓用戶端隨時可以查詢鏈的狀態。 因為這需要大量的儲存空間,我們可以將鏈資料與狀態資料分開: + +- 鏈資料 (區塊、交易列表) +- 狀態資料 (每筆交易狀態轉換的結果) + +狀態根屬於後者,是_隱含_資料 (不儲存在鏈上),而鏈資料是明確的,儲存在鏈本身上 ([來源](https://ethereum.stackexchange.com/questions/359/where-is-the-state-data-stored))。 + +在本教學課程中,我們將著重於可以透過 Dune Analytics 以 SQL 查詢的鏈上資料。 + +如上所述,每個區塊都包含一個交易列表,我們可以透過篩選特定區塊來查詢。 我們來試試最新的區塊,12396854: + +```sql +SELECT * FROM ethereum."transactions" +WHERE block_number = 12396854 +ORDER BY block_time DESC` +``` + +以下是 Dune 上的 SQL 輸出: + +![](./list_of_txn.png) + +這個被新增到鏈上的單一區塊會改變以太坊虛擬機 ([EVM](/developers/docs/evm/)) 的狀態。 有時一次會驗證數十筆,甚至數百筆交易。 在這個特定案例中,共包含了 222 筆交易。 + +若要查看實際上有多少筆交易成功,我們可以新增另一個篩選器來計算成功的交易數量: + +```sql +WITH temp_table AS ( + SELECT * FROM ethereum."transactions" + WHERE block_number = 12396854 AND success = true + ORDER BY block_time DESC +) +SELECT + COUNT(success) AS num_successful_txn +FROM temp_table +``` + +在區塊 12396854 中,總共 222 筆交易裡,有 204 筆成功驗證: + +![](./successful_txn.png) + +交易請求每秒發生數十次,但區塊大約每 15 秒才提交一次 ([來源](/developers/docs/blocks/))。 + +若要查看大約每 15 秒產生一個區塊,我們可以將一天的秒數 (86400) 除以 15,得到每日平均區塊數的估計值 (約 5760)。 + +以太坊每日產生的區塊圖表 (2016 年至今) 如下: + +![](./daily_blocks.png) + +在此期間,每日產生的區塊平均數約為 5,874: + +![](./avg_daily_blocks.png) + +查詢如下: + +```sql +# 查詢以視覺化呈現 2016 年以來每日產生的區塊數量 + +SELECT + DATE_TRUNC('day', time) AS dt, + COUNT(*) AS block_count +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 + +# 每日產生的平均區塊數量 + +WITH temp_table AS ( +SELECT + DATE_TRUNC('day', time) AS dt, + COUNT(*) AS block_count +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +) +SELECT + AVG(block_count) AS avg_block_count +FROM temp_table +``` + +自 2016 年以來,每日產生的平均區塊數為 5,874,略高於該數字。 或者,將 86400 秒除以 5874 個平均區塊,得出 14.7 秒,即大約每 15 秒一個區塊。 + +### Gas {#gas} + +區塊的大小是有限制的。 區塊大小上限是動態的,會根據網路需求在 12,500,000 到 25,000,000 單位之間變化。 需要限制,以防止任意大的區塊對完整節點在磁碟空間和速度要求方面造成壓力 ([來源](/developers/docs/blocks/))。 + +要將區塊 gas 上限概念化,其中一種方式是將其視為可用於批次處理交易的區塊空間的**供給**。 從 2016 年至今的區塊 gas 上限可以查詢並視覺化呈現: + +![](./avg_gas_limit.png) + +```sql +SELECT + DATE_TRUNC('day', time) AS dt, + AVG(gas_limit) AS avg_block_gas_limit +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +``` + +然後是每日實際用於支付以太坊鏈上運算費用的 gas (例如傳送交易、呼叫智能合約、鑄造 NFT)。 這是對可用以太坊區塊空間的**需求**: + +![](./daily_gas_used.png) + +```sql +SELECT + DATE_TRUNC('day', time) AS dt, + AVG(gas_used) AS avg_block_gas_used +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +``` + +我們也可以將這兩個圖表並列,看看**需求與供給**如何對應: + +![gas_demand_supply](./gas_demand_supply.png) + +因此,在供給固定的情況下,我們可以將 gas 價格理解為以太坊區塊空間需求的函數。 + +最後,我們可能想查詢以太坊鏈的每日平均 gas 價格,然而,這樣做會導致查詢時間特別長,所以我們將篩選查詢,只看以太坊基金會每筆交易所支付的平均 gas 金額。 + +![](./ef_daily_gas.png) + +我們可以看見多年來,所有傳送至以太坊基金會地址的交易所支付的 gas 價格。 查詢如下: + +```sql +SELECT + block_time, + gas_price / 1e9 AS gas_price_gwei, + value / 1e18 AS eth_sent +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +``` + +### 總結 {#summary} + +透過本教學課程,我們透過查詢和體驗鏈上資料,了解了基礎的以太坊概念,以及以太坊區塊鏈的運作方式。 + +包含本教學課程中所有程式碼的儀表板可以在[此處](https://dune.com/paulapivat/Learn-Ethereum)找到。 + +若要利用資料進一步探索 Web3,歡迎[在 Twitter 上找到我](https://twitter.com/paulapivat)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/logging-events-smart-contracts/index.md b/public/content/translations/zh-tw/developers/tutorials/logging-events-smart-contracts/index.md new file mode 100644 index 00000000000..97f1a1946b3 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/logging-events-smart-contracts/index.md @@ -0,0 +1,62 @@ +--- +title: "使用事件記錄智能合約資料" +description: "智能合約事件簡介,以及如何使用它們來記錄資料" +author: "jdourlens" +tags: [ "smart contracts", "remix", "solidity", "events" ] +skill: intermediate +lang: zh-tw +published: 2020-04-03 +source: EthereumDev +sourceUrl: https://ethereumdev.io/logging-data-with-events/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +在 Solidity 中,[事件](/developers/docs/smart-contracts/anatomy/#events-and-logs)是智能合約可以發出的信號。 去中心化應用程式或任何連接到 Ethereum JSON-RPC API 的東西,都可以監聽這些事件並採取相應的行動。 事件也可以被索引,以便日後可以搜尋事件歷史。 + +## Events {#events} + +撰寫本文時,Ethereum 區塊鏈上最常見的事件是轉帳事件,當有人轉移代幣時,ERC20 代幣會發出此事件。 + +```solidity +event Transfer(address indexed from, address indexed to, uint256 value); +``` + +事件簽章在合約程式碼內部宣告,並可以使用 `emit` 關鍵字發出。 例如,轉帳事件記錄了誰發送了轉帳 (`_from`)、轉給誰 (`_to`) 以及轉移了多少代幣 (`_value`)。 + +如果我們回到我們的 Counter 智能合約,並決定在每次值變更時都進行記錄。 由於此合約並非用於部署,而是作為透過擴展來建立另一個合約的基礎:它被稱為抽象合約。 在我們的計數器範例中,它會是這個樣子: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + event ValueChanged(uint oldValue, uint256 newValue); + + // 用於儲存計數的私有 unsigned int 變數 + uint256 private count = 0; + + // 增加計數器的函式 + function increment() public { + count += 1; + emit ValueChanged(count - 1, count); + } + + // 用於取得計數值的 Getter 函式 + function getCount() public view returns (uint256) { + return count; + } + +} +``` + +請注意: + +- **第 5 行**:我們宣告了我們的事件以及它包含的內容:舊值和新值。 + +- **第 13 行**:當我們遞增計數變數時,我們會發出事件。 + +如果我們現在部署合約並呼叫 increment 函式,我們會看到,如果你在名為 logs 的陣列中點擊新的交易,Remix 將會自動顯示它。 + +![Remix 螢幕截圖](./remix-screenshot.png) + +日誌對於偵錯你的智能合約非常有用,但如果你建立供不同人使用的應用程式,它們也很重要,可以讓分析更容易,以追蹤和了解你的智能合約是如何被使用的。 交易產生的日誌會顯示在熱門的區塊瀏覽器中,你也可以使用它們來建立鏈下腳本,以監聽特定事件並在事件發生時採取行動。 diff --git a/public/content/translations/zh-tw/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md b/public/content/translations/zh-tw/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md new file mode 100644 index 00000000000..2c4a8747aa5 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md @@ -0,0 +1,244 @@ +--- +title: "用於離線資料完整性的默克爾證明" +description: "為儲存在鏈下的資料確保其鏈上完整性" +author: Ori Pomerantz +tags: [ "storage" ] +skill: advanced +lang: zh-tw +published: 2021-12-30 +--- + +## 介紹 {#introduction} + +理想情況下,我們希望將所有內容儲存在以太坊儲存空間中。以太坊儲存空間分布在數千台電腦上,具有極高的可用性(資料無法被審查)和完整性(資料無法在未經授權的情況下被修改),但儲存一個 32 位元組的字通常需要花費 20,000 gas。 在我撰寫本文時,該成本相當於 6.60 美元。 每位元組 21 美分的價格對於許多用途來說都太過昂貴。 + +為了要解決這個問題,以太坊生態系開發了[許多以去中心化方式儲存資料的替代方案](/developers/docs/storage/)。 通常它們都涉及可用性和價格之間的權衡。 然而,完整性通常能獲得保證。 + +在本文中,您將學習**如何**使用[默克爾證明](https://computersciencewiki.org/index.php/Merkle_proof)在不將資料儲存在區塊鏈上的情況下確保資料完整性。 + +## 它是如何運作的? {#how-does-it-work} + +理論上,我們可以只將資料的哈希儲存在鏈上,並在需要資料的交易中傳送所有資料。 但是,這將會還是太昂貴了。 往一項交易傳送的一個字節位元組的數據會消耗掉約16份燃料,現時相當於半毛錢,或者每千字節會消耗掉約$5。 每 MB 5000 美元的價格,對於許多用途來說仍然太貴,即使不考慮哈希處理資料的額外成本。 + +解決方案是重複哈希處理資料的不同子集,這樣一來,對於您不需要傳送的資料,您只需傳送一個哈希即可。 您可以使用默克爾樹來完成此操作,這是一種樹狀資料結構,其中每個節點都是其下方節點的哈希: + +![默克爾樹](tree.png) + +根哈希是唯一需要儲存在鏈上的部分。 為了證明某個特定值,您需要提供所有與其組合以獲得根所需的哈希。 例如,為了證明 `C`,您需要提供 `D`、`H(A-B)` 和 `H(E-H)`。 + +![C 值的證明](proof-c.png) + +## 實作 {#implementation} + +[範例程式碼在此處提供](https://github.com/qbzzt/merkle-proofs-for-offline-data-integrity)。 + +### 鏈下程式碼 {#offchain-code} + +在本文中,我們使用 JavaScript 進行鏈下運算。 大多數去中心化應用程式都以 JavaScript 撰寫其鏈下元件。 + +#### 建立默克爾根 {#creating-the-merkle-root} + +首先,我們需要往區塊鏈提供Merkle樹根。 + +```javascript +const ethers = require("ethers") +``` + +[我們使用 ethers 套件中的哈希函式](https://docs.ethers.io/v5/api/utils/hashing/#utils-keccak256)。 + +```javascript +// 我們必須驗證其完整性的原始資料。前兩個位元組 +// 是使用者識別碼,後兩個位元組是使用者目前擁有的代幣數量。 +const dataArray = [ + 0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060, + 0xface0070, 0xbad00080, 0x060d0091, +] +``` + +把每個輸入編碼成為一個單一256-bit的整數,會導致產生一個較低閱讀性的程序碼。要舉例的話,那就是比使用JSON編碼而成的數值的閱讀性還要低了。 但是,這意味著要提取合約內的數據需要經歷的程序會大幅降低,所消耗的燃料成本如是。 [您可以在鏈上讀取 JSON](https://github.com/chrisdotn/jsmnSol),但如果可以避免,最好不要這麼做。 + +```javascript +// 哈希值陣列,以 BigInts 形式表示 +const hashArray = dataArray +``` + +在這個例子中,我們的數據是從256-bit的數值開始的,所以不需要更多的程序了。 如果我們使用一個更複雜的數據結構如字串們,我們需要確認我們先雜湊好數據以拿到一個欄目的雜湊值。 要注意這也是因為我們不在意用戶是否知道其他用戶的資訊。 否則,我們將會必須要進行雜湊,所以用戶1將不會知道用戶0的數值,用戶2則將不會知道用戶3的數值,如此類推。 + +```javascript +// 在哈希函式預期的字串和我們在其他地方使用的 +// BigInt 之間進行轉換。 +const hash = (x) => + BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0))) +``` + +ethers 哈希函式預期接收一個帶有十六進位數字的 JavaScript 字串 (例如 `0x60A7`),並回應一個具有相同結構的字串。 然而,對於其餘的程式碼而言,使用 `BigInt` 更為容易,所以我們先轉換為十六進位字串,然後再轉換回來。 + +```javascript +// 一對值的對稱哈希,所以我們不關心順序是否顛倒。 +const pairHash = (a, b) => hash(hash(a) ^ hash(b)) +``` + +這個函式是對稱的 (a [xor](https://en.wikipedia.org/wiki/Exclusive_or) b 的哈希)。 這是指當我們檢查Merkle推論的時候,我們不需要擔心有關是否要把從推論而來的數值安放在計算完成的數值前或後方。 默克爾證明的檢查是在鏈上完成的,所以我們在那裡需要做的事情越少越好。 + +警告: +密碼學比看起來的要困難。 +本文的最初版本使用了哈希函式 `hash(a^b)`。 +那是個**糟糕**的想法,因為這意味著如果您知道 `a` 和 `b` 的合法值,您就可以使用 `b' = a^b^a'` 來證明任何想要的 `a'` 值。 +使用這個函式,您必須計算 `b'` 使得 `hash(a') ^ hash(b')` 等於一個已知值 (通往根路徑上的下一個分支),這要困難得多。 + +```javascript +// 用來表示某個分支為空、沒有 +// 值的值 +const empty = 0n +``` + +當數值的數字不是一個整數再取二次方,我們便需要處理空出來的枝節。 這個程序要這樣寫的原因是把零放左一個空間持有者。 + +![遺失分支的默克爾樹](merkle-empty-hash.png) + +```javascript +// 依序對每對值取哈希,計算出哈希陣列樹的上一層 +const oneLevelUp = (inputArray) => { + var result = [] + var inp = [...inputArray] // 為避免覆寫輸入 // 必要時新增一個空值 (我們需要所有葉節點都 // 成對) + + if (inp.length % 2 === 1) inp.push(empty) + + for (var i = 0; i < inp.length; i += 2) + result.push(pairHash(inp[i], inp[i + 1])) + + return result +} // oneLevelUp +``` + +這個功能會在Merkle樹狀內「攀爬」到一個程度,此舉是透過把現層的數值們作成雜湊值的配對。 請注意,這不是最有效率的實作,我們本可以避免複製輸入,而只在迴圈中適當的時候新增 `hashEmpty`,但此程式碼是為了可讀性而最佳化。 + +```javascript +const getMerkleRoot = (inputArray) => { + var result + + result = [...inputArray] // 沿著樹向上,直到只剩下一個值,那就是 // 根。 // // 如果一層有奇數個條目,oneLevelUp 中的 // 程式碼會新增一個空值,所以如果我們有,例如,// 10 個葉節點,那麼第二層會有 5 個分支,第三層 // 有 3 個分支,第四層有 2 個分支,而根是第五層 + + while (result.length > 1) result = oneLevelUp(result) + + return result[0] +} +``` + +要拿到根部,一直攀爬直到這裡只剩下一個數值。 + +#### 建立默克爾證明 {#creating-a-merkle-proof} + +一個Merkle推論是用來跟一些已經被證明好的數值雜湊在一起,以拿回Merkle樹根。 被證明的數值通常從其他數據可以得手,所以比起作為程序碼的一部份我的取向會是分別提供這些數值。 + +```javascript +// 默克爾證明由要一起哈希的條目清單 +// 的值組成。因為我們使用對稱哈希函式,所以我們不 +// 需要項目的位置來驗證證明,只需要用它來建立證明 +const getMerkleProof = (inputArray, n) => { +    var result = [], currentLayer = [...inputArray], currentN = n + +    // 直到我們到達頂部 +    while (currentLayer.length > 1) { +        // 沒有奇數長度的層 +        if (currentLayer.length % 2) +            currentLayer.push(empty) + +        result.push(currentN % 2 +               // 如果 currentN 是奇數,將它前面的值加入證明 +            ? currentLayer[currentN-1] +               // 如果是偶數,則加入它後面的值 +            : currentLayer[currentN+1]) + +``` + +我們哈希處理 `(v[0],v[1])`、`(v[2],v[3])` 等。 所以對於偶數值,我們需要下一個;對於奇數值,我們需要前一個。 + +```javascript +        // 移動到上一層 +        currentN = Math.floor(currentN/2) +        currentLayer = oneLevelUp(currentLayer) +    }   // while currentLayer.length > 1 + +    return result +}   // getMerkleProof +``` + +### 鏈上程式碼 {#onchain-code} + +最終,我們有了核實推論的程式碼。 鏈上程式碼是以 [Solidity](https://docs.soliditylang.org/en/v0.8.11/) 撰寫的。 最優化在此是更為重要,因為相對來說燃料價格是較為昂貴的。 + +```solidity +//SPDX-License-Identifier: Public Domain +pragma solidity ^0.8.0; + +import "hardhat/console.sol"; +``` + +我是使用 [Hardhat 開發環境](https://hardhat.org/) 撰寫此程式碼的,它讓我們在開發時可以取得[來自 Solidity 的主控台輸出](https://hardhat.org/docs/cookbook/debug-logs)。 + +```solidity + +contract MerkleProof { +    uint merkleRoot; + +    function getRoot() public view returns (uint) { +      return merkleRoot; +    } + +    // 極度不安全,在生產程式碼中,對此函式的存取 +    // **必須**嚴格限制,可能僅限於 +    // 擁有者 +    function setRoot(uint _merkleRoot) external { +      merkleRoot = _merkleRoot; +    }   // setRoot +``` + +為Merkle樹根而設定和拿取功能。 在生產系統中,讓任何人都能更新默克爾根是個_極其糟糕的想法_。 我在此這樣做是為了把範例程式碼給簡化掉。 **不要在資料完整性至關重要的系統上這麼做**。 + +```solidity +    function hash(uint _a) internal pure returns(uint) { +      return uint(keccak256(abi.encode(_a))); +    } + +    function pairHash(uint _a, uint _b) internal pure returns(uint) { +      return hash(hash(_a) ^ hash(_b)); +    } +``` + +這個功能產生了一對雜湊值。 這只是 JavaScript 程式碼中 `hash` 和 `pairHash` 的 Solidity 轉譯。 + +**注意:**這是另一個為了可讀性而進行最佳化的案例。 根據[函式定義](https://www.tutorialspoint.com/solidity/solidity_cryptographic_functions.htm),或許可以將資料儲存為 [`bytes32`](https://docs.soliditylang.org/en/v0.5.3/types.html#fixed-size-byte-arrays) 值並避免轉換。 + +```solidity +    // 驗證默克爾證明 +    function verifyProof(uint _value, uint[] calldata _proof) +        public view returns (bool) { +      uint temp = _value; +      uint i; + +      for(i=0; i<_proof.length; i++) { +        temp = pairHash(temp, _proof[i]); +      } + +      return temp == merkleRoot; +    } + +}  // MarkleProof +``` + +在數學表示法中,默克爾證明驗證看起來像這樣:`H(proof_n, H(proof_n-1, H(proof_n-2, ...` H(proof_1, H(proof_0, value))...)))`。 這個程序碼會實行驗證。 + +## 默克爾證明與匯總值不相容 {#merkle-proofs-and-rollups} + +默克爾證明與[匯總值](/developers/docs/scaling/#rollups) 的相容性不佳。 這個現象的原因是匯總值在層一負責書寫所有的交易數據,但在層二上是負責紀錄交易過程。 伴隨一項交易來傳送一份Merkle推論的每層平均成本是638份燃料(現時在一個回呼內的一個字節位元組會花費16份燃料,如果它數值不是為零的話。如果為零,它會花費掉4份燃料)。 如果我們有1024字的數據,一份Merkle推論需要十層,或是總共6380份燃料。 + +以 [Optimism](https://public-grafana.optimism.io/d/9hkhMxn7z/public-dashboard?orgId=1&refresh=5m) 為例,寫入 L1 的 gas 約為 100 gwei,L2 的 gas 為 0.001 gwei (這是正常價格,可能會隨著擁塞而上漲)。 所以,就層一的成本來說,我們在層二的程序中可以花掉百到千份的燃料。 先假設我們不會寫爆儲存,這達標我們可用層一燃料的價格來在層二寫大約五個字詞。 對於單一的默克爾證明,我們可以將全部 1024 個字寫入儲存空間 (假設它們一開始就可以在鏈上計算,而不是在交易中提供),並且仍然有大部分的 gas 剩餘。 + +## 結論 {#conclusion} + +在現實生活中,你可能永遠不會單靠自己來施行Merkle樹狀理論。 這裡有些著名而被審查好的圖書館給你用。通常來說,最好不要自己來實行虛擬代幣的早期版本。 但是,我希望現在你會更好地理解到Merkle推論,並且能夠決定甚麼時候它們會值得被使用。 + +請注意,雖然默克爾證明保留了_完整性_,但它們不保留_可用性_。 當我們知道沒有其他人可以拿取你的資產時,這個行為在兩種情境中可以成為你一個小小的悲傷。其一是當數據儲存否決你的接觸許可,其二是反回來看你不能構造一個Merkle樹來接觸到儲存。 所以,Merkle樹最好是跟某種如IPFS的去中央化儲存一起使用。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md b/public/content/translations/zh-tw/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md new file mode 100644 index 00000000000..a7db97eab52 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md @@ -0,0 +1,151 @@ +--- +title: "使用 InfluxDB 與 Grafana 監控 Geth" +description: "使用 InfluxDB 與 Grafana 設定 Geth 節點的監控,以追蹤效能並識別問題。" +author: "Mario Havel" +tags: [ "clients", "nodes" ] +skill: intermediate +lang: zh-tw +published: 2021-01-13 +--- + +本教學將協助您設定 Geth 節點的監控,以便更深入地了解其效能並識別潛在問題。 + +## 先決條件 {#prerequisites} + +- 您應已在執行一個 Geth 實例。 +- 大部分步驟和範例都適用於 Linux 環境,具備基本的終端機知識將會很有幫助。 +- 請觀看此 Geth 指標套件的影片概覽:[Monitoring an Ethereum infrastructure by Péter Szilágyi](https://www.youtube.com/watch?v=cOBab8IJMYI)。 + +## 監控堆疊 {#monitoring-stack} + +以太坊用戶端會收集大量資料,這些資料可以按時間順序資料庫的形式讀取。 為簡化監控,您可以將這些資料饋入資料視覺化軟體。 有以下幾種選項可供選擇: + +- [Prometheus](https://prometheus.io/) (提取模型) +- [InfluxDB](https://www.influxdata.com/get-influxdb/) (推送模型) +- [Telegraf](https://www.influxdata.com/get-influxdb/) +- [Grafana](https://www.grafana.com/) +- [Datadog](https://www.datadoghq.com/) +- [Chronograf](https://www.influxdata.com/time-series-platform/chronograf/) + +此外,還有 [Geth Prometheus Exporter](https://github.com/hunterlong/gethexporter),這是一個預先配置好 InfluxDB 和 Grafana 的選項。 + +在本教學中,我們將設定您的 Geth 用戶端,將資料推送到 InfluxDB 以建立資料庫,並推送到 Grafana 以建立資料的圖形化視覺呈現。 手動操作有助於您更深入地了解流程、修改流程,並在不同環境中部署。 + +## 設定 InfluxDB {#setting-up-influxdb} + +首先,我們來下載並安裝 InfluxDB。 您可以在 [Influxdata 發布頁面](https://portal.influxdata.com/downloads/) 找到各種下載選項。 請選擇適合您環境的版本。 +您也可以從 [儲存庫](https://repos.influxdata.com/) 安裝。 例如,在 Debian 系列的發行版中: + +``` +curl -tlsv1.3 --proto =https -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add +source /etc/lsb-release +echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list +sudo apt update +sudo apt install influxdb -y +sudo systemctl enable influxdb +sudo systemctl start influxdb +sudo apt install influxdb-client +``` + +成功安裝 InfluxDB 後,請確保它在背景執行。 依預設,可以在 `localhost:8086` 連線到它。 +在使用 `influx` 用戶端之前,您必須建立一個具有管理員權限的新使用者。 此使用者將用於高階管理、建立資料庫和使用者。 + +``` +curl -XPOST "http://localhost:8086/query" --data-urlencode "q=CREATE USER username WITH PASSWORD 'password' WITH ALL PRIVILEGES" +``` + +現在您可以使用此使用者透過 influx 用戶端進入 [InfluxDB shell](https://docs.influxdata.com/influxdb/v1.8/tools/shell/)。 + +``` +influx -username 'username' -password 'password' +``` + +在其 shell 中直接與 InfluxDB 通訊,您可以為 geth 指標建立資料庫和使用者。 + +``` +create database geth +create user geth with password choosepassword +``` + +使用以下指令驗證已建立的項目: + +``` +show databases +show users +``` + +離開 InfluxDB shell。 + +``` +exit +``` + +InfluxDB 正在執行並已設定為儲存來自 Geth 的指標。 + +## 準備 Geth {#preparing-geth} + +設定好資料庫後,我們需要在 Geth 中啟用指標收集。 請注意 `geth --help` 中的 `METRICS AND STATS OPTIONS`。 您可以在那裡找到多個選項,在這種情況下,我們希望 Geth 將資料推送到 InfluxDB。 +基本設定指定了 InfluxDB 的可連線端點和資料庫的驗證資訊。 + +``` +geth --metrics --metrics.influxdb --metrics.influxdb.endpoint "http://0.0.0.0:8086" --metrics.influxdb.username "geth" --metrics.influxdb.password "chosenpassword" +``` + +這些旗標可以附加到啟動用戶端的指令中,或儲存到設定檔中。 + +您可以透過列出資料庫中的指標等方式,來驗證 Geth 是否成功推送資料。 在 InfluxDB shell 中: + +``` +use geth +show measurements +``` + +## 設定 Grafana {#setting-up-grafana} + +下一步是安裝 Grafana,它將以圖形方式解譯資料。 請在 Grafana 文件中遵循您環境的安裝程序。 如果您沒有其他需求,請務必安裝 OSS 版本。 +使用儲存庫為 Debian 發行版安裝的範例步驟: + +``` +curl -tlsv1.3 --proto =https -sL https://packages.grafana.com/gpg.key | sudo apt-key add - +echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list +sudo apt update +sudo apt install grafana +sudo systemctl enable grafana-server +sudo systemctl start grafana-server +``` + +當 Grafana 執行後,應該可以在 `localhost:3000` 連線到它。 +使用您偏好的瀏覽器存取此路徑,然後使用預設憑證登入 (使用者:`admin`,密碼:`admin`)。 出現提示時,請變更預設密碼並儲存。 + +![](./grafana1.png) + +您將被重新導向到 Grafana 首頁。 首先,設定您的資料來源。 按一下左側欄的設定圖示,然後選取 "Data sources"。 + +![](./grafana2.png) + +目前尚未建立任何資料來源,請按一下 "Add data source" 來定義一個。 + +![](./grafana3.png) + +對於此設定,請選取 "InfluxDB" 並繼續。 + +![](./grafana4.png) + +如果您在同一台機器上執行工具,資料來源的設定非常直接。 您需要設定 InfluxDB 位址和存取資料庫的詳細資訊。 請參考下圖。 + +![](./grafana5.png) + +如果一切都已完成且 InfluxDB 可連線,請按一下 "Save and test",然後等待確認訊息彈出。 + +![](./grafana6.png) + +Grafana 現在已設定為可從 InfluxDB 讀取資料。 現在您需要建立一個儀表板來解譯和顯示資料。 儀表板屬性被編碼在 JSON 檔案中,任何人都可以建立並輕鬆匯入這些檔案。 在左側欄上,按一下 "Create and Import"。 + +![](./grafana7.png) + +若要建立 Geth 監控儀表板,請複製[此儀表板](https://grafana.com/grafana/dashboards/13877/)的 ID,並將其貼到 Grafana 的 "Import page" 中。 儲存儀表板後,它應該看起來像這樣: + +![](./grafana8.png) + +您可以修改您的儀表板。 每個面板都可以編輯、移動、移除或新增。 您可以變更您的設定。 一切由您決定! 要了解更多關於儀表板的運作方式,請參考 [Grafana 的文件](https://grafana.com/docs/grafana/latest/dashboards/)。 +您可能也對[警示](https://grafana.com/docs/grafana/latest/alerting/)感興趣。 這可讓您設定當指標達到特定值時的警示通知。 支援多種通訊管道。 diff --git a/public/content/translations/zh-tw/developers/tutorials/nft-minter/index.md b/public/content/translations/zh-tw/developers/tutorials/nft-minter/index.md new file mode 100644 index 00000000000..9f58b99afd0 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/nft-minter/index.md @@ -0,0 +1,866 @@ +--- +title: "非同質化代幣鑄幣機使用教學" +description: "在本教學中,你將建構一個 NFT 鑄造器,並學習如何透過 MetaMask 和 Web3 工具將你的智能合約連接到 React 前端,來建立一個全端去中心化應用程式。" +author: "smudgil" +tags: [ "solidity", "NFT", "alchemy", "smart contracts", "frontend", "Pinata" ] +skill: intermediate +lang: zh-tw +published: 2021-10-06 +--- + +對於來自 Web2 背景的開發人員來說,最大的挑戰之一是弄清楚如何將你的智慧型合約連接到前端專案並與之交互。 + +通過構建非同質化代幣鑄幣機 ,一個可以用來輸入數字資產鏈接、名稱與描述的簡單用戶界面 — 你將學會如何: + +- 通過你的前端專案連接到 MetaMask +- 在前端調用智慧型合約的方法 +- 使用MetaMask簽署交易 + +在本教學中,我們將使用 [React](https://react.dev/) 作為前端框架。 因為此教程最初是專注於 Web3 的發展上,我們將不會花費過多的時間來解釋 React 的基本概念。 反之,我們將會集中在往我們專案帶來功能的方法。 + +作為一個前提,你理應有初學者對 React 的理解 - 會知道組件、道具、useState/useEffect,還有基本功能回呼的運作。 如果你之前從未聽過這些術語,不妨先看看這篇 [React 入門教學](https://react.dev/learn/tutorial-tic-tac-toe)。 對於偏好視覺學習的讀者,我們強烈推薦由 Net Ninja 製作的這套優質影片系列:[Full Modern React Tutorial](https://www.youtube.com/playlist?list=PL4cUxeGkcC9gZD-Tvwfod2gaISzfRiP9d)。 + +而且如果你還沒準備好,你且將會一定需要一個Alchemy的帳戶來完成該份教程,也會在區塊鏈上建立任何東西。 [在此](https://alchemy.com/)註冊一個免費帳戶。 + +迫不及待了嗎?那麼讓我們開始吧! + +## NFT 製作入門 {#making-nfts-101} + +在我們甚至要開始著眼在任何程式碼前,理解創造一個NFT的運作過程是重要的。 它由兩個步驟組成: + +### 在以太坊區塊鏈上發布 NFT 智能合約 {#publish-nft} + +在兩份NFT智慧型合約標準中最大的分別是:ERC-1155協議是一份具有多種代幣標準且內有批次的功能的合約;對比來看ERC-721協議只是一份單一代幣標準的合約,也因此它只會支持一次性地傳送一個代幣。 + +### 呼叫鑄造函式 {#minting-function} + +通常,這個鑄造函式需要你傳入兩個變數作為參數:第一個是 `recipient`,用來指定接收你新鑄造 NFT 的位址;第二個是 NFT 的 `tokenURI`,這是一個字串,會解析成描述 NFT 元資料的 JSON 文件。 + +一個NFT的元數據是把NFT從虛擬帶到現實的東西,它容許NFT持有特質,如一個名字、描述、圖像(或不同的電子資產),還有其他屬性。 這是 [一個 tokenURI 的範例](https://gateway.pinata.cloud/ipfs/QmSvBcb4tjdFpajGJhbFAWeK3JAxCdNQLQtr6ZdiSi42V2),其中包含了 NFT 的元資料。 + +在此教程內,我們將會專注於第二部分,它是有關使用我們的 React UI 呼叫一個現存 NFT 智慧型合約的鑄造功能。 + +[這個連結](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE) 是我們在本教學中將會呼叫的 ERC-721 NFT 智能合約。 如果你想學習我們是如何製作它的,我們強烈推薦你查看我們的另一篇教學:["如何建立一個 NFT"](https://www.alchemy.com/docs/how-to-create-an-nft)。 + +好的,現在我們明白了讓一個NFT運作的原理,讓我們複製我們的起始檔案們吧! + +## 複製入門檔案 {#clone-the-starter-files} + +首先,前往 [nft-minter-tutorial GitHub 儲存庫](https://github.com/alchemyplatform/nft-minter-tutorial) 以取得此專案的入門檔案。 將此儲存庫複製到你的本機環境中。 + +當你開啟這個複製的 `nft-minter-tutorial` 儲存庫時,你會注意到它包含兩個資料夾:`minter-starter-files` 和 `nft-minter`。 + +- `minter-starter-files` 包含此專案的入門檔案 (主要是 React UI)。 在本教學中,**我們將會在這個目錄中工作**,你將學習如何將此 UI 連接到你的以太坊錢包和 NFT 智能合約,讓它活起來。 +- `nft-minter` 包含整個已完成的教學,如果你卡關了,可以把它當作**參考**。 + +接著,在你的程式碼編輯器中開啟你的 `minter-starter-files` 複本,然後導覽至 `src` 資料夾。 + +我們將撰寫的所有程式碼都會放在 `src` 資料夾底下。 我們將會編輯 `Minter.js` 元件並撰寫額外的 javascript 檔案,為我們的專案提供 Web3 功能。 + +## 步驟 2:查看我們的入門檔案 {#step-2-check-out-our-starter-files} + +在我們開始打碼前,在起始檔案裡面檢查一下有甚麼是已經提供給我們發展專案是很重要的。 + +### 讓你的 React 專案動起來 {#get-your-react-project-running} + +讓我們透過在瀏覽器內執行這個 React 專案來開始今天的教程吧。 React 的優點在於一旦我們在瀏覽器內已經有在運行自己的專案,我們儲存下來的任何改變都將會被即時更新到瀏覽器裡。 + +要讓專案動起來,請導覽至 `minter-starter-files` 資料夾的根目錄,然後在你的終端機中執行 `npm install` 來安裝專案的依賴項: + +```bash +cd minter-starter-files +npm install +``` + +安裝完成後,在你的終端機中執行 `npm start`: + +```bash +npm start +``` + +此舉理應會在你的瀏覽器內打開網站(http://localhost:3000/),在那裡你將會看見我們專案的前端。 它應該由三個欄位組成:一個輸入往你的NFT資產所在地的連結空位、一個輸入你的NFT名字的空格,以及一個提供形容段落的欄位。 + +如你試圖點擊按鈕「Connect Wallet」或「Mint NFT」,你將會注意到它們不會如常運作 - 這是因為我們將仍然需要設計一下它們的功能! :\) + +### Minter.js 元件 {#minter-js} + +**注意:** 請確保你位在 `minter-starter-files` 資料夾,而不是 `nft-minter` 資料夾! + +讓我們回到編輯器中的 `src` 資料夾,並開啟 `Minter.js` 檔案。 這個動作在我們理解該檔案內所有東西上有著超級關鍵的作用,因為它是我們將會首先處理的第一個 React 組件。 + +在此檔案的最頂部,有著我們將會在特定事件後更新的一些狀態變數。 + +```javascript +//狀態變數 +const [walletAddress, setWallet] = useState("") +const [status, setStatus] = useState("") +const [name, setName] = useState("") +const [description, setDescription] = useState("") +const [url, setURL] = useState("") +``` + +從來沒有聽過 React 狀態變數或者狀態鉤子嗎? 查看[這些](https://legacy.reactjs.org/docs/hooks-state.html)文件。 + +在此是每個變數代表的意思: + +- `walletAddress` - 一個儲存使用者錢包位址的字串 +- `status` - 一個包含訊息的字串,會顯示在 UI 底部 +- `name` - 一個儲存 NFT 名稱的字串 +- `description` - 一個儲存 NFT 描述的字串 +- `url` - 一個連結到 NFT 數位資產的字串 + +在狀態變數之後,你會看到三個未實作的函式:`useEffect`、`connectWalletPressed` 和 `onMintPressed`。 你會注意到所有這些函式都是 `async`,這是因為我們將在其中進行非同步的 API 呼叫! 它們的名稱是以各自的功能來命名的: + +```javascript +useEffect(async () => { + //TODO: 實作 +}, []) + +const connectWalletPressed = async () => { + //TODO: 實作 +} + +const onMintPressed = async () => { + //TODO: 實作 +} +``` + +- [`useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html) - 這是一個 React hook,會在你的元件渲染後被呼叫。 因為它傳入了一個空陣列 `[]` 的 prop (見第 3 行),所以它只會在元件的_第一次_渲染時被呼叫。 在此我們將會回呼我們的錢包聽眾以及其他錢包功能來更新我們的UI,去反思一下一個錢包是否已經被連結好。 +- `connectWalletPressed` - 這個函式將被呼叫,用來將使用者的 MetaMask 錢包連接到我們的去中心化應用程式。 +- `onMintPressed` - 這個函式將被呼叫,用來鑄造使用者的 NFT。 + +接近這份檔案的尾聲,我們得到了我們組件的UI。 如果你仔細查看這段程式碼,你會注意到當對應的文字欄位中的輸入改變時,我們會更新 `url`、`name` 和 `description` 這些狀態變數。 + +你也會看到當 ID 為 `mintButton` 和 `walletButton` 的按鈕被點擊時,`connectWalletPressed` 和 `onMintPressed` 會分別被呼叫。 + +```javascript +//我們元件的 UI +return ( +
+ + +

+

🧙‍♂️ Alchemy NFT 鑄造器

+

+ 只需加入你資產的連結、名稱和描述,然後按下「鑄造」。 +

+
+

🖼 資產連結:

+ setURL(event.target.value)} + /> +

🤔 名稱:

+ setName(event.target.value)} + /> +

✍️ 描述:

+ setDescription(event.target.value)} + /> +
+ +

{status}

+
+) +``` + +最後,讓我們強調一下這個Minter組件會在哪裡被添加。 + +如果你查看 `App.js` 檔案,這是 React 中的主要元件,作為所有其他元件的容器,你會看到我們的 Minter 元件在第 7 行被注入。 + +**在本教學中,我們只會編輯 `Minter.js` 檔案,並在 `src` 資料夾中新增檔案。** + +現在我們明白了到底是在跟甚麼東西一起操作後,讓我們設定自己的以太虛擬坊錢包吧! + +## 設定你的以太坊錢包 {#set-up-your-ethereum-wallet} + +使用者需要將他們的以太坊錢包連接到你的去中心化應用程式,才能與你的智能合約互動。 + +### 下載 MetaMask {#download-metamask} + +為此教學,我們將會使用 MetaMask。它是一個在瀏覽器上管理你的乙太坊帳戶地址的虛擬錢包。 如果你想更深入了解以太坊上的交易如何運作,請查看[此頁面](/developers/docs/transactions/)。 + +您可以在[這裡](https://metamask.io/download)免費下載並建立 MetaMask 帳戶。 當你正在創建一個帳戶時,或者如果你已經持有一個帳戶,你要確定在右上方把模式轉移到「Ropsten測試網路」之上 \(因為這樣我們才不會處理到真實金錢\)。 + +### 從水龍頭取得以太幣 {#add-ether-from-faucet} + +為了要鑄造我們的 NFT(或是簽署任何在以太坊區塊鏈的交易),我們將需要一些假 ETH。 要取得 Eth,你可以前往 [Ropsten 水龍頭](https://faucet.ropsten.be/),輸入你的 Ropsten 帳戶位址,然後點擊「Send Ropsten Eth」。 你應該很快便能在你的MetaMask帳戶裡看見ETH! + +### 檢查你的餘額 {#check-your-balance} + +為了再次確認我們的餘額,讓我們使用 [Alchemy 的 composer 工具](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D) 發出一個 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 請求。 這將會把我們錢包內的以太結餘回傳。 在你輸入自己的MetaMask帳戶地址,並且點下「寄送請求」後,你理應會看見一個這樣子的回應: + +```text +{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"} +``` + +**注意:** 此結果的單位是 wei,不是 eth。 Wei是一個被用來計算以太最少分數的單位。 要把wei換算到ETH的算術是:1 ETH = 10¹⁸ wei。 所以,如果我們要換算 0xde0b6b3a7640000 到小數點,我們會得到 1\*10¹⁸的結果,它相當於一個ETH的數值。 + +哈! 我們的假錢都在這! + +## 將 MetaMask 連接到你的 UI {#connect-metamask-to-your-UI} + +既然我們的 MetaMask 錢包已經設定好了,就讓我們把去中心化應用程式連接到它吧! + +因為我們想遵循 [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 範式,所以我們要建立一個獨立的檔案,其中包含管理我們去中心化應用程式的邏輯、資料和規則的函式,然後將這些函式傳遞給我們的前端 (我們的 Minter.js 元件)。 + +### `connectWallet` 函式 {#connect-wallet-function} + +為此,讓我們在你的 `src` 目錄中建立一個名為 `utils` 的新資料夾,並在其中加入一個名為 `interact.js` 的檔案,它將包含我們所有錢包和智能合約的互動函式。 + +在我們的 `interact.js` 檔案中,我們將撰寫一個 `connectWallet` 函式,然後在我們的 `Minter.js` 元件中匯入並呼叫它。 + +在你的 `interact.js` 檔案中,加入以下內容 + +```javascript +export const connectWallet = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_requestAccounts", + }) + const obj = { + status: "👆🏽 請在上方文字欄位中寫入一則訊息。", + address: addressArray[0], + } + return obj + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 你必須在瀏覽器中安裝 MetaMask,一個虛擬的以太坊錢包。 + +

+
+ ), + } + } +} +``` + +讓我們一起分解這個程式碼的工作: + +首先,我們的函式會檢查你的瀏覽器中是否啟用了 `window.ethereum`。 + +`window.ethereum` 是由 MetaMask 和其他錢包提供者注入的全域 API,允許網站請求使用者的以太坊帳戶。 如果請求被接納,它能夠從連結成功的用戶那裡的區塊鏈中讀取數據,並且會向用戶提出簽署訊息和交易的建議。 查看 [MetaMask 文件](https://docs.metamask.io/guide/ethereum-provider.html#table-of-contents)以獲得更多資訊! + +如果 `window.ethereum` _不存在_,那表示 MetaMask 沒有安裝。 這會回傳一個 JSON 物件,其中回傳的 `address` 是一個空字串,而 `status` JSX 物件則傳達使用者必須安裝 MetaMask 的訊息。 + +**我們撰寫的大部分函式都會回傳 JSON 物件,我們可以用它來更新我們的狀態變數和 UI。** + +現在如果 `window.ethereum` _存在_,事情就變得有趣了。 + +使用 try/catch 迴圈,我們將透過呼叫 [`window.ethereum.request({ method: "eth_requestAccounts" });`](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts) 來嘗試連接 MetaMask。 呼叫這個函式會在瀏覽器中開啟 MetaMask,提示使用者將他們的錢包連接到你的去中心化應用程式。 + +- 如果使用者選擇連接,`method: "eth_requestAccounts"` 會回傳一個陣列,其中包含所有連接到此去中心化應用程式的使用者帳戶位址。 總而言之,我們的 `connectWallet` 函式會回傳一個 JSON 物件,其中包含此陣列中的_第一個_ `address` (見第 9 行) 和一則 `status` 訊息,提示使用者向智能合約寫入一則訊息。 +- 如果使用者拒絕連接,那麼 JSON 物件將包含一個空字串作為回傳的 `address`,以及一則反映使用者拒絕連接的 `status` 訊息。 + +### 將 connectWallet 函式新增至你的 Minter.js UI 元件 {#add-connect-wallet} + +既然我們已經寫好了 `connectWallet` 函式,就把它連接到我們的 `Minter.js` 元件吧。 + +首先,我們必須將函式匯入到 `Minter.js` 檔案中,方法是在 `Minter.js` 檔案的頂部新增 `import { connectWallet } from "./utils/interact.js";`。 你的 `Minter.js` 的前 11 行現在應該看起來像這樣: + +```javascript +import { useEffect, useState } from "react"; +import { connectWallet } from "./utils/interact.js"; + +const Minter = (props) => { + + //狀態變數 + const [walletAddress, setWallet] = useState(""); + const [status, setStatus] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [url, setURL] = useState(""); +``` + +然後,在我們的 `connectWalletPressed` 函式內部,我們將呼叫匯入的 `connectWallet` 函式,如下所示: + +```javascript +const connectWalletPressed = async () => { + const walletResponse = await connectWallet() + setStatus(walletResponse.status) + setWallet(walletResponse.address) +} +``` + +注意到我們大部分的功能是如何從 `interact.js` 檔案中抽象出來,與 `Minter.js` 元件分離的嗎? 這是我們跟M-V-C規範相容的做法! + +在 `connectWalletPressed` 中,我們只需對匯入的 `connectWallet` 函式進行一個 await 呼叫,並利用其回應,透過它們的 state hooks 更新我們的 `status` 和 `walletAddress` 變數。 + +現在,讓我們儲存 `Minter.js` 和 `interact.js` 這兩個檔案,並測試一下我們目前的 UI。 + +在「localhost:3000」打開你的瀏覽器,並在頁面的右上方按下按鍵「連結起錢包」。 + +如果你已安裝 MetaMask,系統應該會提示你將錢包連接到你的去中心化應用程式。 請接受進行連結的邀請。 + +你應會看得到錢包的按鈕現時反映了你的地址被連結好了。 + +接下來,試著重新整理頁面... 這很奇怪。 我們的錢包按鈕會鼓勵我們對MetaMask進行連結,就算它已經被連結好了。。。。。。 + +但是,請不要擔心! 我們可以透過實作一個名為 `getCurrentWalletConnected` 的函式輕鬆解決這個問題,該函式會檢查是否有位址已經連接到我們的去中心化應用程式,並相應地更新我們的 UI! + +### getCurrentWalletConnected 函式 {#get-current-wallet} + +在你的 `interact.js` 檔案中,新增以下 `getCurrentWalletConnected` 函式: + +```javascript +export const getCurrentWalletConnected = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_accounts", + }) + if (addressArray.length > 0) { + return { + address: addressArray[0], + status: "👆🏽 請在上方文字欄位中寫入一則訊息。", + } + } else { + return { + address: "", + status: "🦊 請使用右上角的按鈕連接到 MetaMask。", + } + } + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 你必須在瀏覽器中安裝 MetaMask,一個虛擬的以太坊錢包。 + +

+
+ ), + } + } +} +``` + +這段程式碼與我們剛才寫的 `connectWallet` 函式_非常_相似。 + +主要的區別在於,我們不是呼叫 `eth_requestAccounts` 方法 (這會開啟 MetaMask 讓使用者連接他們的錢包),而是在這裡呼叫 `eth_accounts` 方法,它只會回傳一個包含當前連接到我們去中心化應用程式的 MetaMask 位址的陣列。 + +為了看到這個函式的實際作用,讓我們在 `Minter.js` 元件的 `useEffect` 函式中呼叫它。 + +就像我們對 `connectWallet` 所做的一樣,我們必須將這個函式從 `interact.js` 檔案匯入到 `Minter.js` 檔案中,如下所示: + +```javascript +import { useEffect, useState } from "react" +import { + connectWallet, + getCurrentWalletConnected, //在此匯入 +} from "./utils/interact.js" +``` + +現在,我們只需在 `useEffect` 函式中呼叫它: + +```javascript +useEffect(async () => { + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) +}, []) +``` + +注意,我們使用對 `getCurrentWalletConnected` 呼叫的回應來更新我們的 `walletAddress` 和 `status` 狀態變數。 + +一旦你已經添加好了這個程式碼,嘗試刷新我們的瀏覽器視窗。 這個按鈕應該會跟你說:「你已經連結好了。」,然後會顯出一個你錢包被連結好的地址的預視 - 就算在你刷新之後也會這樣! + +### 實作 addWalletListener {#implement-add-wallet-listener} + +我們去中心化應用程式錢包設定的最後一個步驟是實作錢包監聽器,這樣當我們錢包的狀態改變時 (例如使用者中斷連線或切換帳戶),我們的 UI 就會更新。 + +在你的 `Minter.js` 檔案中,新增一個 `addWalletListener` 函式,如下所示: + +```javascript +function addWalletListener() { + if (window.ethereum) { + window.ethereum.on("accountsChanged", (accounts) => { + if (accounts.length > 0) { + setWallet(accounts[0]) + setStatus("👆🏽 請在上方文字欄位中寫入一則訊息。") + } else { + setWallet("") + setStatus("🦊 請使用右上角的按鈕連接到 MetaMask。") + } + }) + } else { + setStatus( +

+ {" "} + 🦊 + 你必須在瀏覽器中安裝 MetaMask,一個虛擬的以太坊錢包。 + +

+ ) + } +} +``` + +讓我們趕快分析在這裡發生的事情: + +- 首先,我們的函式會檢查 `window.ethereum` 是否已啟用 (即 MetaMask 已安裝)。 + - 如果沒有啟用,我們只需將 `status` 狀態變數設定為一個 JSX 字串,提示使用者安裝 MetaMask。 + - 如果已啟用,我們在第 3 行設定監聽器 `window.ethereum.on("accountsChanged")`,它會監聽 MetaMask 錢包的狀態變化,包括使用者將額外帳戶連接到去中心化應用程式、切換帳戶或中斷帳戶連線時。 如果至少有一個帳戶已連接,`walletAddress` 狀態變數會更新為監聽器回傳的 `accounts` 陣列中的第一個帳戶。 否則,`walletAddress` 會被設定為空字串。 + +最後,我們必須在 `useEffect` 函式中呼叫它: + +```javascript +useEffect(async () => { + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) + + addWalletListener() +}, []) +``` + +然後就沒有然後了! 我們已經完成了所有有關我們錢包功能的編程! 現在我們的錢包已被設定好,讓我們探索一下怎樣鑄造我們的 NFT 吧! + +## NFT 元資料入門 {#nft-metadata-101} + +那麼記得我們剛才在此教程的第零步驟提到的NFT元數據 - 它把一個虛擬的NFT帶到現實生活中,容許它持有特質,像是一個電子資產、名稱、描述,還有其他屬性。 + +我們需要將此元資料設定為一個 JSON 物件並儲存它,這樣我們就可以在呼叫智能合約的 `mintNFT` 函式時,將它作為 `tokenURI` 參數傳入。 + +在欄位的文字:「往資產的連結」、「名稱」,「描述」將會由我們NFT元數據的不同特質而組成。 我們將會把這些文字整理成為一個JSON客體,但是那裡有能讓我們能夠選擇把這個客體儲存好的幾個位置: + +- 我們可以把它安放在以太坊區塊鏈上;但是,這樣做的花費會非常昂貴。 +- 我們可以把它儲存在一個中心化服務器上,像 AWS或Firebase。 但是那樣可能會違背我們的去中央化宗旨。 +- 我們也可以使用IPFS,它是一個去中央化的規條和朋輩間的網路,讓我們在一個分配檔案系統內儲存及分享數據。 因為這個規條是去中央化和免費的,它是我們的最佳選項! + +為了將我們的元資料儲存在 IPFS 上,我們將使用 [Pinata](https://pinata.cloud/),這是一個方便的 IPFS API 和工具包。 在下一步,我們將會逐步解釋怎樣使用它! + +## 使用 Pinata 將你的元資料釘選到 IPFS {#use-pinata-to-pin-your-metadata-to-IPFS} + +如果你沒有 [Pinata](https://pinata.cloud/) 帳戶,請[在此](https://app.pinata.cloud/auth/signup)註冊一個免費帳戶,並完成驗證你的電子郵件和帳戶的步驟。 + +### 建立你的 Pinata API 金鑰 {#create-pinata-api-key} + +導覽至 [https://pinata.cloud/keys](https://pinata.cloud/keys) 頁面,然後選取頂部的「New Key」按鈕,將 Admin 小工具設定為啟用,並為你的金鑰命名。 + +你便會看見一個彈出視窗,內有你的API資訊。 確保把這個鑰匙放在某個安全的地方。 + +現在我們的鑰匙被設定好了,讓我們把它添加到專案中來使用它。 + +### 建立一個 .env 檔案 {#create-a-env} + +我們能夠在一個環境檔案中安全地儲存我們的Pinata金鑰和祕密。 讓我們在你的專案目錄中安裝 [dotenv 套件](https://www.npmjs.com/package/dotenv)。 + +在你的終端機中開啟一個新分頁 (與執行 local host 的分頁分開),並確保你在 `minter-starter-files` 資料夾中,然後在終端機中執行以下指令: + +```text +npm install dotenv --save +``` + +接下來,在你的 `minter-starter-files` 的根目錄中建立一個 `.env` 檔案,方法是在你的指令列中輸入以下內容: + +```javascript +vim.env +``` + +這會在 vim (一個文字編輯器) 中開啟你的 `.env` 檔案。 在你的鍵盤上以這個順序來點擊按鍵「esc」+「:」+「q」進行儲存。 + +接下來,在 VSCode 中,導覽至你的 `.env` 檔案,並將你的 Pinata API 金鑰和 API 密鑰加入其中,如下所示: + +```text +REACT_APP_PINATA_KEY = +REACT_APP_PINATA_SECRET = +``` + +儲存檔案,然後你就準備好開始為了上載自己JSON元數據到IPFS而書寫的功能了! + +### 實作 pinJSONToIPFS {#pin-json-to-ipfs} + +幸運的是,Pinata 有一個[專門用於將 JSON 資料上傳到 IPFS 的 API](https://docs.pinata.cloud/api-reference/endpoint/ipfs/pin-json-to-ipfs#pin-json) 和一個方便的 JavaScript 與 axios 範例,我們稍作修改即可使用。 + +在你的 `utils` 資料夾中,讓我們建立另一個名為 `pinata.js` 的檔案,然後從 .env 檔案匯入我們的 Pinata 密鑰和金鑰,如下所示: + +```javascript +require("dotenv").config() +const key = process.env.REACT_APP_PINATA_KEY +const secret = process.env.REACT_APP_PINATA_SECRET +``` + +接下來,將下面額外的程式碼貼到你的 `pinata.js` 檔案中。 不用擔心,我們將會把每個步驟都挑出來細說! + +```javascript +require("dotenv").config() +const key = process.env.REACT_APP_PINATA_KEY +const secret = process.env.REACT_APP_PINATA_SECRET + +const axios = require("axios") + +export const pinJSONToIPFS = async (JSONBody) => { + const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS` + //向 Pinata 發送 axios POST 請求 ⬇️ + return axios + .post(url, JSONBody, { + headers: { + pinata_api_key: key, + pinata_secret_api_key: secret, + }, + }) + .then(function (response) { + return { + success: true, + pinataUrl: + "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash, + } + }) + .catch(function (error) { + console.log(error) + return { + success: false, + message: error.message, + } + }) +} +``` + +所以,這個程式碼到底是用來做甚麼的呢? + +首先,它匯入了 [axios](https://www.npmjs.com/package/axios),這是一個基於 promise 的 HTTP 用戶端,適用於瀏覽器和 node.js,我們將用它來向 Pinata 發出請求。 + +然後我們有我們的非同步函式 `pinJSONToIPFS`,它接收一個 `JSONBody` 作為其輸入,並在其標頭中包含 Pinata API 金鑰和密鑰,所有這些都是為了向他們的 `pinJSONToIPFS` API 發出 POST 請求。 + +- 如果這個 POST 請求成功,那麼我們的函式會回傳一個 JSON 物件,其中 `success` 布林值為 true,以及我們元資料被釘選的 `pinataUrl`。 我們將使用這個回傳的 `pinataUrl` 作為我們智能合約鑄造函式的 `tokenURI` 輸入。 +- 如果此 post 請求失敗,那麼我們的函式會回傳一個 JSON 物件,其中 `success` 布林值為 false,以及一個傳達我們錯誤的 `message` 字串。 + +與我們的 `connectWallet` 函式回傳類型一樣,我們回傳 JSON 物件,以便我們可以使用它們的參數來更新我們的狀態變數和 UI。 + +## 載入你的智能合約 {#load-your-smart-contract} + +既然我們有辦法透過我們的 `pinJSONToIPFS` 函式將我們的 NFT 元資料上傳到 IPFS,我們就需要一種方法來載入我們智能合約的實例,以便我們可以呼叫它的 `mintNFT` 函式。 + +如前所述,在本教學中,我們將使用[這個現有的 NFT 智能合約](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE);然而,如果你想學習我們是如何製作它的,或自己製作一個,我們強烈建議你查看我們的另一篇教學,["如何建立一個 NFT"](https://www.alchemy.com/docs/how-to-create-an-nft)。 + +### 合約 ABI {#contract-abi} + +如果你仔細檢查過我們的檔案,你會注意到在我們的 `src` 目錄中,有一個 `contract-abi.json` 檔案。 為了特別註明哪一個功能會將被一份合約啟發,並且保障這個功能將會回返在你預計體裁內的數據,一個ABI是達成此目的基本條件。 + +我們也將會需要一個Alchemy的API鑰匙,還有Alchemy Web3的API來連結到以太坊區塊鏈,並且把我們的智慧型合約上載。 + +### 建立你的 Alchemy API 金鑰 {#create-alchemy-api} + +如果你還沒有 Alchemy 帳戶,[請在此免費註冊](https://alchemy.com/?a=eth-org-nft-minter)。 + +一旦你已經創建好一個Alchemy的帳戶,你可以通過建立一個程式來生成一個API鑰匙。 這將會允許我們發送請求到Ropsten的測試網上。 + +在你的「Alchemy里程表」內導航至頁面「創建程式」(“Create App”)。這動作能夠透過把在導航列條內的選項「程式」懸停,並減低「創建程式」來完成。 + +命名你的程式。我們已經選擇好「我的最初NFT!」作為名稱,提供一個短段描述,選取「盤點」來選擇為你的程式藏書的理想環境,並選擇「Ropsten」作為你的網路。 + +點擊「創建程式」然後就好了! 你的程式應該會在下列圖表中出現。 + +非常好,所以現在我們已經創建好我們的HTTP的Alchemy,API超連結。把它複製到你的剪貼板。。。。。。 + +…然後讓我們把它加到我們的 `.env` 檔案中。 好了,你的 .env檔案應該是像這個樣子的: + +```text +REACT_APP_PINATA_KEY = +REACT_APP_PINATA_SECRET = +REACT_APP_ALCHEMY_KEY = https://eth-ropsten.alchemyapi.io/v2/ +``` + +現在我們有了合約 ABI 和 Alchemy API 金鑰,我們準備好使用 [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3) 來載入我們的智能合約了。 + +### 設定你的 Alchemy Web3 端點和合約 {#setup-alchemy-endpoint} + +首先,如果你還沒有安裝 [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3),你需要到終端機的主目錄 `nft-minter-tutorial` 來安裝它: + +```text +cd .. +npm install @alch/alchemy-web3 +``` + +接下來讓我們回到 `interact.js` 檔案。 在檔案的頂部,新增下列程式碼從你的 .env檔案中輸入你的Alchemy金鑰,並且設定你Alchemy Web3的重點: + +```javascript +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) +``` + +[Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3) 是 [Web3.js](https://docs.web3js.org/) 的一個包裝器,提供了增強的 API 方法和其他關鍵優勢,讓你的 web3 開發者生活更輕鬆。 它是被設計成最低配置,因此你能夠在你的應用程式內馬上開始使用它! + +之後,讓我們往我們的檔案裡添加我們的合約ABI以及合約地址。 + +```javascript +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE" +``` + +一旦我們有以上兩者,我們已經準備好開始為鑄造功能進行編碼了! + +## 實作 mintNFT 函式 {#implement-the-mintnft-function} + +在你的 `interact.js` 檔案中,讓我們定義我們的函式 `mintNFT`,顧名思義,它將鑄造我們的 NFT。 + +因為我們將製作無數的非同步回呼 \(往Pinata釘下我們往IPFS,Alchemy Web3上載的元數據來載入我們的智慧型合約,以及MetaMask來為我們的交易簽署\),我們的功能將同時被設定為非同步的。 + +我們函式的三個輸入將是我們數位資產的 `url`、`name` 和 `description`。 在 `connectWallet` 函式下方新增以下函式簽章: + +```javascript +export const mintNFT = async (url, name, description) => {} +``` + +### 輸入錯誤處理 {#input-error-handling} + +自然地,在功能開始時持有某些輸入錯誤的處理方法是合乎常理的。所以如果我們的輸入指標不正確的話,我們會離開此功能。 在我們的功能內,讓我們新增以下程式碼: + +```javascript +export const mintNFT = async (url, name, description) => { + //錯誤處理 + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗鑄造前請確保所有欄位都已填寫完畢。", + } + } +} +``` + +基本上,如果任何輸入參數是空字串,那麼我們會回傳一個 JSON 物件,其中 `success` 布林值為 false,而 `status` 字串則傳達我們 UI 中的所有欄位都必須填寫完整。 + +### 將元資料上傳至 IPFS {#upload-metadata-to-ipfs} + +一旦我們知道我們的元資料格式正確,下一步就是將它包裝成一個 JSON 物件,並透過我們寫的 `pinJSONToIPFS` 將它上傳到 IPFS! + +為此,我們首先需要將 `pinJSONToIPFS` 函式匯入到我們的 `interact.js` 檔案中。 在 `interact.js` 的最頂部,讓我們新增: + +```javascript +import { pinJSONToIPFS } from "./pinata.js" +``` + +回想一下,`pinJSONToIPFS` 接收一個 JSON 主體。 所以在我們呼叫它之前,我們需要將我們的 `url`、`name` 和 `description` 參數格式化成一個 JSON 物件。 + +讓我們更新我們的程式碼,以建立一個名為 `metadata` 的 JSON 物件,然後使用這個 `metadata` 參數呼叫 `pinJSONToIPFS`: + +```javascript +export const mintNFT = async (url, name, description) => { + //錯誤處理 + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗鑄造前請確保所有欄位都已填寫完畢。", + } + } + + //製作元資料 + const metadata = new Object() + metadata.name = name + metadata.image = url + metadata.description = description + + //進行 pinata 呼叫 + const pinataResponse = await pinJSONToIPFS(metadata) + if (!pinataResponse.success) { + return { + success: false, + status: "😢 上傳你的 tokenURI 時出了點問題。", + } + } + const tokenURI = pinataResponse.pinataUrl +} +``` + +注意,我們將對 `pinJSONToIPFS(metadata)` 的呼叫回應儲存在 `pinataResponse` 物件中。 然後,我們對這個客體進行語法分析以檢查任何可能的語法錯誤。 + +如果有錯誤,我們會回傳一個 JSON 物件,其中 `success` 布林值為 false,而我們的 `status` 字串則傳達我們的呼叫失敗。 否則,我們從 `pinataResponse` 中提取 `pinataURL` 並將其儲存為我們的 `tokenURI` 變數。 + +現在是時候使用Alchemy Web3的API來上載我們的智慧型合約。我們把它們在自家檔案的頂部進行了初始化。 將以下程式碼行新增到 `mintNFT` 函式的底部,以在 `window.contract` 全域變數上設定合約: + +```javascript +window.contract = await new web3.eth.Contract(contractABI, contractAddress) +``` + +最後要加入我們 `mintNFT` 函式的是我們的以太坊交易: + +```javascript +//設定你的以太坊交易 +const transactionParameters = { + to: contractAddress, // 必填,除非是合約發布。 + from: window.ethereum.selectedAddress, // 必須與使用者目前的位址相符。 + data: window.contract.methods + .mintNFT(window.ethereum.selectedAddress, tokenURI) + .encodeABI(), //呼叫 NFT 智能合約 +} + +//透過 MetaMask 簽署交易 +try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + success: true, + status: + "✅ 在 Etherscan 上查看你的交易:https://ropsten.etherscan.io/tx/" + + txHash, + } +} catch (error) { + return { + success: false, + status: "😥 出了點問題:" + error.message, + } +} +``` + +如你已經對以太坊的交易很熟悉了,你將會注意到這個結構跟你以前看過的那些結構是蠻相近的。 + +- 首先,我們設定好我們交易的指標們。 + - `to` 指定接收方位址 (我們的智能合約) + - `from` 指定交易的簽署者 (使用者連接到 MetaMask 的位址:`window.ethereum.selectedAddress`) + - `data` 包含對我們智能合約 `mintNFT` 方法的呼叫,該方法接收我們的 `tokenURI` 和使用者錢包位址 `window.ethereum.selectedAddress` 作為輸入 +- 然後,我們進行一個 await 呼叫 `window.ethereum.request`,我們要求 MetaMask 簽署交易。 注意,在這個請求中,我們指定了我們的 eth 方法 (eth_SentTransaction) 並傳入了我們的 `transactionParameters`。 在這時機,MetaMask將會在瀏覽器中被開啟,然後鼓勵用戶去簽署或拒絕該筆交易。 + - 如果交易成功,函式將回傳一個 JSON 物件,其中布林值 `success` 設定為 true,而 `status` 字串則提示使用者查看 Etherscan 以獲取有關其交易的更多資訊。 + - 如果交易失敗,函式將回傳一個 JSON 物件,其中 `success` 布林值設定為 false,而 `status` 字串則傳達錯誤訊息。 + +總而言之,我們的 `mintNFT` 函式應該看起來像這樣: + +```javascript +export const mintNFT = async (url, name, description) => { + //錯誤處理 + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗鑄造前請確保所有欄位都已填寫完畢。", + } + } + + //製作元資料 + const metadata = new Object() + metadata.name = name + metadata.image = url + metadata.description = description + + //pinata 釘選請求 + const pinataResponse = await pinJSONToIPFS(metadata) + if (!pinataResponse.success) { + return { + success: false, + status: "😢 上傳你的 tokenURI 時出了點問題。", + } + } + const tokenURI = pinataResponse.pinataUrl + + //載入智能合約 + window.contract = await new web3.eth.Contract(contractABI, contractAddress) //loadContract(); + + //設定你的以太坊交易 + const transactionParameters = { + to: contractAddress, // 必填,除非是合約發布。 + from: window.ethereum.selectedAddress, // 必須與使用者目前的位址相符。 + data: window.contract.methods + .mintNFT(window.ethereum.selectedAddress, tokenURI) + .encodeABI(), //呼叫 NFT 智能合約 + } + + //透過 MetaMask 簽署交易 + try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + success: true, + status: + "✅ 在 Etherscan 上查看你的交易:https://ropsten.etherscan.io/tx/" + + txHash, + } + } catch (error) { + return { + success: false, + status: "😥 出了點問題:" + error.message, + } + } +} +``` + +那是一個大型功能呀! 現在,我們只需要將我們的 `mintNFT` 函式連接到我們的 `Minter.js` 元件... + +## 將 mintNFT 連接到我們的 Minter.js 前端 {#connect-our-frontend} + +打開你的 `Minter.js` 檔案,並將頂部的 `import { connectWallet, getCurrentWalletConnected } from "./utils/interact.js";` 行更新為: + +```javascript +import { + connectWallet, + getCurrentWalletConnected, + mintNFT, +} from "./utils/interact.js" +``` + +最後,實作 `onMintPressed` 函式,以對你匯入的 `mintNFT` 函式進行 await 呼叫,並更新 `status` 狀態變數以反映我們的交易是成功還是失敗: + +```javascript +const onMintPressed = async () => { + const { status } = await mintNFT(url, name, description) + setStatus(status) +} +``` + +## 將你的 NFT 部署到線上網站 {#deploy-your-NFT} + +準備好實時把你的專案提供給用戶們去進行互動了嗎? 查看[此教學](https://docs.alchemy.com/alchemy/tutorials/nft-minter/how-do-i-deploy-nfts-online)以將你的鑄造器部署到線上網站。 + +最終一步。。。。。。 + +## 席捲區塊鏈世界 {#take-the-blockchain-world-by-storm} + +開玩笑的,你成功堅持到教程的最終點了! + +可以透過建立一個 NFT 鑄造器來複習,你已經成功學會怎樣: + +- 通過你的前端專案連接到 MetaMask +- 在前端調用智慧型合約的方法 +- 使用MetaMask簽署交易 + +想必你會希望能夠在錢包中展示透過你的去中心化應用程式所鑄造的 NFT——所以請務必查看我們的快速教學 [如何在你的錢包中查看你的 NFT](https://www.alchemy.com/docs/how-to-view-your-nft-in-your-mobile-wallet)! + +一如既往,如果你有任何問題,我們會在 [Alchemy Discord](https://discord.gg/gWuC7zB) 提供協助。 我們不能更迫切地看見你是怎樣運用此篇教程的概念來建立自己在不遠未來的專案們了! diff --git a/public/content/translations/zh-tw/developers/tutorials/optimism-std-bridge-annotated-code/index.md b/public/content/translations/zh-tw/developers/tutorials/optimism-std-bridge-annotated-code/index.md new file mode 100644 index 00000000000..b9e338b0d5d --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/optimism-std-bridge-annotated-code/index.md @@ -0,0 +1,1331 @@ +--- +title: "Optimism 標準跨鏈橋合約深入解析" +description: "Optimism 的標準跨鏈橋如何運作? 為什麼它會這樣運作?" +author: Ori Pomerantz +tags: [ "solidity", "bridge", "layer 2" ] +skill: intermediate +published: 2022-03-30 +lang: zh-tw +--- + +[Optimism](https://www.optimism.io/) 是一種[樂觀卷軸](/developers/docs/scaling/optimistic-rollups/)。 +樂觀卷軸能以遠低於以太坊主網 (又稱為第一層或 L1) 的價格處理交易,因為交易只由少數節點處理,而非網路上每個節點都處理。 +同時,所有資料都會寫入 L1,因此所有內容都能以主網的完整性和可用性保證來證明和重建。 + +若要在 Optimism (或任何其他 L2) 上使用 L1 資產,這些資產需要被[橋接](/bridges/#prerequisites)。 +達成此目的的其中一種方法是讓使用者在 L1 鎖定資產 (最常見的是 ETH 和 [ERC-20 代幣](/developers/docs/standards/tokens/erc-20/)),並在 L2 收到等值的資產來使用。 +最後,無論是誰持有這些資產,都可能會想把它們橋接回 L1。 +這麼做的時候,資產會在 L2 被銷毀,然後在 L1 釋放給使用者。 + +這就是 [Optimism 標準跨鏈橋](https://docs.optimism.io/app-developers/bridging/standard-bridge)的運作方式。 +在本文中,我們將檢視該跨鏈橋的原始程式碼,以了解其運作方式,並將其作為編寫精良的 Solidity 程式碼範例來研究。 + +## 控制流程 {#control-flows} + +該跨鏈橋有兩個主要流程: + +- 存款 (從 L1 到 L2) +- 提款 (從 L2 到 L1) + +### 存款流程 {#deposit-flow} + +#### 第一層 {#deposit-flow-layer-1} + +1. 如果存入 ERC-20,存款人會給予跨鏈橋一筆額度,以花費正在存入的金額 +2. 存款人呼叫 L1 跨鏈橋 (`depositERC20`、`depositERC20To`、`depositETH` 或 `depositETHTo`) +3. L1 跨鏈橋取得橋接資產的所有權 + - ETH:資產由存款人作為呼叫的一部分進行轉移 + - ERC-20:跨鏈橋使用存款人提供的額度將資產轉移給自己 +4. L1 跨鏈橋使用跨網域訊息機制在 L2 跨鏈橋上呼叫 `finalizeDeposit` + +#### 第二層 {#deposit-flow-layer-2} + +5. L2 跨鏈橋驗證對 `finalizeDeposit` 的呼叫是合法的: + - 來自跨網域訊息合約 + - 最初來自 L1 上的跨鏈橋 +6. L2 跨鏈橋檢查 L2 上的 ERC-20 代幣合約是否正確: + - L2 合約回報其 L1 對應合約與代幣在 L1 上的來源合約相同 + - L2 合約回報其支援正確的介面 ([使用 ERC-165](https://eips.ethereum.org/EIPS/eip-165))。 +7. 如果 L2 合約是正確的,則呼叫它以鑄造適當數量的代幣到適當的地址。 若否,則啟動提款程序,讓使用者可以在 L1 上取回代幣。 + +### 提款流程 {#withdrawal-flow} + +#### 第二層 {#withdrawal-flow-layer-2} + +1. 提款人呼叫 L2 跨鏈橋 (`withdraw` 或 `withdrawTo`) +2. L2 跨鏈橋銷毀屬於 `msg.sender` 的適當數量的代幣 +3. L2 跨鏈橋使用跨網域訊息機制在 L1 跨鏈橋上呼叫 `finalizeETHWithdrawal` 或 `finalizeERC20Withdrawal` + +#### 第一層 {#withdrawal-flow-layer-1} + +4. L1 跨鏈橋驗證對 `finalizeETHWithdrawal` 或 `finalizeERC20Withdrawal` 的呼叫是合法的: + - 來自跨網域訊息機制 + - 最初來自 L2 上的跨鏈橋 +5. L1 跨鏈橋將適當的資產 (ETH 或 ERC-20) 轉移到適當的地址 + +## 第一層程式碼 {#layer-1-code} + +這是在 L1 (以太坊主網) 上執行的程式碼。 + +### IL1ERC20Bridge {#IL1ERC20Bridge} + +[此介面定義在此](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol)。 +它包含橋接 ERC-20 代幣所需的功能和定義。 + +```solidity +// SPDX-License-Identifier: MIT +``` + +[Optimism 的大部分程式碼都是在 MIT 授權條款下釋出](https://help.optimism.io/hc/en-us/articles/4411908707995-What-software-license-does-Optimism-use-)。 + +```solidity +pragma solidity >0.5.0 <0.9.0; +``` + +在撰寫本文時,Solidity 的最新版本為 0.8.12。 +在 0.9.0 版本釋出前,我們不知道此程式碼是否與其相容。 + +```solidity +/** + * @title IL1ERC20Bridge + */ +interface IL1ERC20Bridge { + /********** + * 事件 * + **********/ + + event ERC20DepositInitiated( +``` + +在 Optimism 跨鏈橋的術語中,_deposit_ 指從 L1 到 L2 的轉移,而 _withdrawal_ 則指從 L2 到 L1 的轉移。 + +```solidity + address indexed _l1Token, + address indexed _l2Token, +``` + +在大多數情況下,L1 上的 ERC-20 地址與 L2 上對等的 ERC-20 地址不同。 +[您可以在此處查看代幣地址清單](https://static.optimism.io/optimism.tokenlist.json)。 +`chainId` 為 1 的地址在 L1 (主網) 上,`chainId` 為 10 的地址在 L2 (Optimism) 上。 +另外兩個 `chainId` 值分別用於 Kovan 測試網 (42) 和 Optimistic Kovan 測試網 (69)。 + +```solidity + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); +``` + +可以為轉移新增註記,在這種情況下,它們會被新增到回報它們的事件中。 + +```solidity + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); +``` + +同一個跨鏈橋合約處理雙向的轉移。 +在 L1 跨鏈橋的情況下,這表示存款的初始化和提款的最終確定。 + +```solidity + + /******************** + * 公用函式 * + ********************/ + + /** + * @dev 取得相應 L2 跨鏈橋合約的地址。 + * @return 相應 L2 跨鏈橋合約的地址。 + */ + function l2TokenBridge() external returns (address); +``` + +這個函式並非真正必要,因為在 L2 上它是一個預先部署的合約,所以地址永遠是 `0x4200000000000000000000000000000000000010`。 +它在這裡是為了與 L2 跨鏈橋對稱,因為 L1 跨鏈橋的地址並不容易知道。 + +```solidity + /** + * @dev 將一定數量的 ERC20 存入呼叫者在 L2 上的餘額。 + * @param _l1Token 我們正在存入的 L1 ERC20 的地址 + * @param _l2Token L1 各自在 L2 的 ERC20 地址 + * @param _amount 要存入的 ERC20 數量 + * @param _l2Gas 在 L2 上完成存款所需的 Gas 限制。 + * @param _data 可選資料,轉發到 L2。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; +``` + +`_l2Gas` 參數是交易允許花費的 L2 Gas 數量。 +[在某個 (高) 限制內,這是免費的](https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions-2),所以除非 ERC-20 合約在鑄造時做了非常奇怪的事情,否則應該不成問題。 +這個函式處理常見的情境,即使用者將資產橋接到不同區塊鏈上的同一個地址。 + +```solidity + /** + * @dev 將一定數量的 ERC20 存入收款人在 L2 上的餘額。 + * @param _l1Token 我們正在存入的 L1 ERC20 的地址 + * @param _l2Token L1 各自在 L2 的 ERC20 地址 + * @param _to 將提款記入的 L2 地址。 + * @param _amount 要存入的 ERC20 數量。 + * @param _l2Gas 在 L2 上完成存款所需的 Gas 限制。 + * @param _data 可選資料,轉發到 L2。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; +``` + +這個函式與 `depositERC20` 幾乎相同,但它允許您將 ERC-20 傳送到不同的地址。 + +```solidity + /************************* + * 跨鏈函式 * + *************************/ + + /** + * @dev 完成從 L2 到 L1 的提款,並將資金記入收款人在 + * L1 ERC20 代幣的餘額。 + * 如果從 L2 初始化的提款尚未最終確定,此呼叫將會失敗。 + * + * @param _l1Token 要為其 finalizeWithdrawal 的 L1 代幣地址。 + * @param _l2Token 提款初始化的 L2 代幣地址。 + * @param _from 啟動轉移的 L2 地址。 + * @param _to 將提款記入的 L1 地址。 + * @param _amount 要存入的 ERC20 數量。 + * @param _data 由傳送者在 L2 上提供的資料。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external; +} +``` + +在 Optimism 中,提款 (以及其他從 L2 到 L1 的訊息) 是一個兩步驟的過程: + +1. 在 L2 上進行一筆啟動交易。 +2. 在 L1 上進行一筆最終確定或領取的交易。 + 此交易需要在 L2 交易的[錯誤挑戰期](https://community.optimism.io/docs/how-optimism-works/#fault-proofs)結束後進行。 + +### IL1StandardBridge {#il1standardbridge} + +[此介面定義在此](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/IL1StandardBridge.sol)。 +此檔案包含 ETH 的事件和函式定義。 +這些定義與上面在 `IL1ERC20Bridge` 中為 ERC-20 定義的非常相似。 + +跨鏈橋介面被分成兩個檔案,因為一些 ERC-20 代幣需要自訂處理,無法由標準跨鏈橋處理。 +這樣,處理此類代幣的自訂跨鏈橋可以實作 `IL1ERC20Bridge`,而無需同時橋接 ETH。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +import "./IL1ERC20Bridge.sol"; + +/** + * @title IL1StandardBridge + */ +interface IL1StandardBridge is IL1ERC20Bridge { + /********** + * 事件 * + **********/ + event ETHDepositInitiated( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); +``` + +此事件與 ERC-20 版本 (`ERC20DepositInitiated`) 幾乎相同,只是沒有 L1 和 L2 的代幣地址。 +其他事件和函式也是如此。 + +```solidity + event ETHWithdrawalFinalized( + . + . + . + ); + + /******************** + * 公用函式 * + ********************/ + + /** + * @dev 將一定數量的 ETH 存入呼叫者在 L2 上的餘額。 + . + . + . + */ + function depositETH(uint32 _l2Gas, bytes calldata _data) external payable; + + /** + * @dev 將一定數量的 ETH 存入收款人在 L2 上的餘額。 + . + . + . + */ + function depositETHTo( + address _to, + uint32 _l2Gas, + bytes calldata _data + ) external payable; + + /************************* + * 跨鏈函式 * + *************************/ + + /** + * @dev 完成從 L2 到 L1 的提款,並將資金記入收款人在 L1 ETH 代幣的餘額。 + * 由於只有 xDomainMessenger 可以呼叫此函式,因此在提款最終確定之前,它永遠不會被呼叫。 + . + . + . + */ + function finalizeETHWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external; +} +``` + +### CrossDomainEnabled {#crossdomainenabled} + +[此合約](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol)由兩個跨鏈橋 ([L1](#the-l1-bridge-contract) 和 [L2](#the-l2-bridge-contract)) 繼承,用於向另一層傳送訊息。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +/* 介面匯入 */ +import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol"; +``` + +[此介面](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/bridge/ICrossDomainMessenger.sol) 告訴合約如何使用跨網域信使向另一層傳送訊息。 +這個跨網域信使是另一個完整的系統,值得單獨寫一篇文章,我希望將來能寫。 + +```solidity +/** + * @title CrossDomainEnabled + * @dev 執行跨網域通訊合約的輔助合約 + * + * 使用的編譯器:由繼承合約定義 + */ +contract CrossDomainEnabled { + /************* + * 變數 * + *************/ + + // 用於從其他網域傳送和接收訊息的信使合約。 + address public messenger; + + /*************** + * 建構函式 * + ***************/ + + /** + * @param _messenger 當前層上 CrossDomainMessenger 的地址。 + */ + constructor(address _messenger) { + messenger = _messenger; + } +``` + +合約需要知道的一個參數是此層上跨網域信使的地址。 +這個參數在建構函式中只設定一次,永不改變。 + +```solidity + + /********************** + * 函式修飾符 * + **********************/ + + /** + * 強制被修改的函式只能由特定的跨網域帳戶呼叫。 + * @param _sourceDomainAccount 原始網域上唯一被授權呼叫此函式的帳戶。 + */ + modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) { +``` + +跨網域訊息傳遞可由其執行所在的區塊鏈 (以太坊主網或 Optimism) 上的任何合約存取。 +但我們需要每一側的跨鏈橋都_只_信任來自另一側跨鏈橋的特定訊息。 + +```solidity + require( + msg.sender == address(getCrossDomainMessenger()), + "OVM_XCHAIN: messenger contract unauthenticated" + ); +``` + +只有來自適當的跨網域信使 (`messenger`,如下所示) 的訊息才能被信任。 + +```solidity + + require( + getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount, + "OVM_XCHAIN: wrong sender of cross-domain message" + ); +``` + +跨網域信使提供傳送訊息至另一層地址的方式是透過 [`.xDomainMessageSender()` 函式](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol#L122-L128)。 +只要它在由該訊息啟動的交易中被呼叫,它就可以提供此資訊。 + +我們需要確保我們收到的訊息來自另一個跨鏈橋。 + +```solidity + + _; + } + + /********************** + * 內部函式 * + **********************/ + + /** + * 通常從儲存中取得信使。公開此函式是為了以防子合約需要覆寫。 + * @return 應該使用的跨網域信使合約的地址。 + */ + function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) { + return ICrossDomainMessenger(messenger); + } +``` + +此函式回傳跨網域信使。 +我們使用函式而不是變數 `messenger`,以允許繼承自此合約的合約使用演算法來指定要使用哪個跨網域信使。 + +```solidity + + /** + * 向另一個網域上的帳戶傳送訊息 + * @param _crossDomainTarget 目標網域上的預期收款人 + * @param _message 要傳送給目標的資料 (通常是傳給帶有 `onlyFromCrossDomainAccount()` 函式的 calldata) + * @param _gasLimit 在目標網域上接收訊息的 gasLimit。 + */ + function sendCrossDomainMessage( + address _crossDomainTarget, + uint32 _gasLimit, + bytes memory _message +``` + +最後,是傳送訊息到另一層的函式。 + +```solidity + ) internal { + // slither-disable-next-line reentrancy-events, reentrancy-benign +``` + +[Slither](https://github.com/crytic/slither) 是一個靜態分析器,Optimism 在每個合約上執行它以尋找漏洞和其他潛在問題。 +在這種情況下,下面這一行會觸發兩個漏洞: + +1. [重入事件](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3) +2. [良性重入](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-2) + +```solidity + getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit); + } +} +``` + +在這種情況下,我們不擔心重入,我們知道 `getCrossDomainMessenger()` 會回傳一個可信的地址,即使 Slither 無法知道這一點。 + +### L1 跨鏈橋合約 {#the-l1-bridge-contract} + +[此合約的原始程式碼在此](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol)。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +``` + +介面可以是其他合約的一部分,所以它們必須支援多種 Solidity 版本。 +但跨鏈橋本身是我們的合約,我們可以嚴格規定它使用的 Solidity 版本。 + +```solidity +/* 介面匯入 */ +import { IL1StandardBridge } from "./IL1StandardBridge.sol"; +import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol"; +``` + +[IL1ERC20Bridge](#IL1ERC20Bridge) 和 [IL1StandardBridge](#IL1StandardBridge) 已在上面解釋。 + +```solidity +import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol"; +``` + +[此介面](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol) 讓我們能夠建立訊息來控制 L2 上的標準跨鏈橋。 + +```solidity +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +``` + +[此介面](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) 讓我們能夠控制 ERC-20 合約。 +[您可以在此處閱讀更多相關資訊](/developers/tutorials/erc20-annotated-code/#the-interface)。 + +```solidity +/* 函式庫匯入 */ +import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol"; +``` + +[如上所述](#crossdomainenabled),此合約用於層間訊息傳遞。 + +```solidity +import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol"; +``` + +[`Lib_PredeployAddresses`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol) 包含 L2 合約的地址,這些合約的地址永遠相同。 這包括 L2 上的標準跨鏈橋。 + +```solidity +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +``` + +[OpenZeppelin 的 Address 公用程式](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol)。 它用於區分合約地址和屬於外部擁有帳戶 (EOA) 的地址。 + +請注意,這不是一個完美的解決方案,因為無法區分直接呼叫和從合約建構函式中進行的呼叫,但至少這讓我們能夠識別並防止一些常見的使用者錯誤。 + +```solidity +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +``` + +[ERC-20 標準](https://eips.ethereum.org/EIPS/eip-20) 支援兩種合約回報失敗的方式: + +1. 還原 +2. 回傳 `false` + +處理這兩種情況會讓我們的程式碼更複雜,所以我們改用 [OpenZeppelin 的 `SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol),它能確保[所有失敗都會導致還原](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L96)。 + +```solidity +/** + * @title L1StandardBridge + * @dev L1 ETH 和 ERC20 跨鏈橋是一個合約,用於儲存存入的 L1 資金和在 L2 上使用的標準代幣。 + * 它同步一個對應的 L2 跨鏈橋,通知其存款並監聽新最終確定的提款。 + * + */ +contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled { + using SafeERC20 for IERC20; +``` + +這一行指定了每次使用 `IERC20` 介面時都要使用 `SafeERC20` 包裝器。 + +```solidity + + /******************************** + * 外部合約參考 * + ********************************/ + + address public l2TokenBridge; +``` + +[L2StandardBridge](#the-l2-bridge-contract) 的地址。 + +```solidity + + // 將 L1 代幣對應到 L2 代幣,再對應到存入的 L1 代幣餘額 + mapping(address => mapping(address => uint256)) public deposits; +``` + +像這樣的雙重[對應](https://www.tutorialspoint.com/solidity/solidity_mappings.htm)是定義[二維稀疏陣列](https://en.wikipedia.org/wiki/Sparse_matrix)的方式。 +此資料結構中的值被識別為 `deposit[L1 token addr][L2 token addr]`。 +預設值為零。 +只有設定為不同值的儲存單元會被寫入儲存空間。 + +```solidity + + /*************** + * 建構函式 * + ***************/ + + // 此合約位於代理之後,因此建構函式參數將不會被使用。 + constructor() CrossDomainEnabled(address(0)) {} +``` + +為了能夠升級此合約而無需複製儲存空間中的所有變數。 +為此,我們使用 [`Proxy`](https://docs.openzeppelin.com/contracts/3.x/api/proxy),這是一個使用 [`delegatecall`](https://solidity-by-example.org/delegatecall/) 將呼叫轉移到另一個獨立合約的合約,該合約的地址由代理合約儲存 (當您升級時,您會告訴代理合約更改該地址)。 +當您使用 `delegatecall` 時,儲存空間仍然是_呼叫_合約的儲存空間,因此所有合約狀態變數的值都不會受到影響。 + +這種模式的一個影響是 `delegatecall` 的_被呼叫_合約的儲存空間不會被使用,因此傳遞給它的建構函式值並不重要。 +這就是我們可以向 `CrossDomainEnabled` 建構函式提供一個無意義值的原因。 +這也是下面的初始化與建構函式分開的原因。 + +```solidity + /****************** + * 初始化 * + ******************/ + + /** + * @param _l1messenger 用於跨鏈通訊的 L1 信使地址。 + * @param _l2TokenBridge L2 標準跨鏈橋地址。 + */ + // slither-disable-next-line external-function +``` + +這個 [Slither 測試](https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external) 會找出未從合約程式碼中呼叫的函式,因此可以宣告為 `external` 而非 `public`。 +`external` 函式的 Gas 成本可能較低,因為它們可以在 calldata 中提供參數。 +宣告為 `public` 的函式必須能從合約內部存取。 +合約無法修改自己的 calldata,所以參數必須在記憶體中。 +當此類函式從外部被呼叫時,需要將 calldata 複製到記憶體,這會消耗 Gas。 +在這種情況下,函式只被呼叫一次,所以效率低下的問題對我們不重要。 + +```solidity + function initialize(address _l1messenger, address _l2TokenBridge) public { + require(messenger == address(0), "Contract has already been initialized."); +``` + +`initialize` 函式應該只被呼叫一次。 +如果 L1 跨網域信使或 L2 代幣跨鏈橋的地址發生變化,我們會建立一個新的代理和一個新的跨鏈橋來呼叫它。 +除非整個系統升級,否則這種情況不太可能發生,這是一個非常罕見的事件。 + +請注意,此函式沒有任何限制_誰_可以呼叫它的機制。 +這意味著理論上,攻擊者可以等到我們部署代理和第一個版本的跨鏈橋後,然後[搶先交易 (front-run)](https://solidity-by-example.org/hacks/front-running/),在合法使用者之前執行 `initialize` 函式。 但有兩種方法可以防止這種情況: + +1. 如果合約不是直接由 EOA 部署,而是[在一個由另一個合約建立它們的交易中部署](https://medium.com/upstate-interactive/creating-a-contract-with-a-smart-contract-bdb67c5c8595),整個過程可以是原子性的,並在任何其他交易執行之前完成。 +2. 如果對 `initialize` 的合法呼叫失敗,總是可以忽略新建立的代理和跨鏈橋,並建立新的。 + +```solidity + messenger = _l1messenger; + l2TokenBridge = _l2TokenBridge; + } +``` + +這是跨鏈橋需要知道的兩個參數。 + +```solidity + + /************** + * 存款 * + **************/ + + /** @dev 修飾符,要求傳送者為 EOA。此檢查可被惡意合約透過 initcode 繞過, + * 但它能處理我們想要避免的使用者錯誤。 + */ + modifier onlyEOA() { + // 用於阻止來自合約的存款 (避免意外遺失代幣) + require(!Address.isContract(msg.sender), "Account not EOA"); + _; + } +``` + +這就是我們需要 OpenZeppelin 的 `Address` 公用程式的原因。 + +```solidity + /** + * @dev 此函式可以在沒有資料的情況下被呼叫, + * 以將一定數量的 ETH 存入呼叫者在 L2 上的餘額。 + * 由於 receive 函式不接受資料,一個保守的預設數量會被轉發到 L2。 + */ + receive() external payable onlyEOA { + _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes("")); + } +``` + +此函式用於測試目的。 +請注意,它並未出現在介面定義中——它不是為正常使用而設計的。 + +```solidity + /** + * @inheritdoc IL1StandardBridge + */ + function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA { + _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data); + } + + /** + * @inheritdoc IL1StandardBridge + */ + function depositETHTo( + address _to, + uint32 _l2Gas, + bytes calldata _data + ) external payable { + _initiateETHDeposit(msg.sender, _to, _l2Gas, _data); + } +``` + +這兩個函式是 `_initiateETHDeposit` 的包裝器,後者處理實際的 ETH 存款。 + +```solidity + /** + * @dev 透過儲存 ETH 並通知 L2 ETH 閘道存款的邏輯。 + * @param _from 從 L1 提取存款的帳戶。 + * @param _to 在 L2 上給予存款的帳戶。 + * @param _l2Gas 在 L2 上完成存款所需的 Gas 限制。 + * @param _data 可選資料,轉發到 L2。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function _initiateETHDeposit( + address _from, + address _to, + uint32 _l2Gas, + bytes memory _data + ) internal { + // 為 finalizeDeposit 呼叫建構 calldata + bytes memory message = abi.encodeWithSelector( +``` + +跨網域訊息的運作方式是,目標合約被呼叫時,會將訊息作為其 calldata。 +Solidity 合約始終根據 [ABI 規範](https://docs.soliditylang.org/en/v0.8.12/abi-spec.html)來解釋其 calldata。 +Solidity 函式 [`abi.encodeWithSelector`](https://docs.soliditylang.org/en/v0.8.12/units-and-global-variables.html#abi-encoding-and-decoding-functions) 建立該 calldata。 + +```solidity + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + _from, + _to, + msg.value, + _data + ); +``` + +這裡的訊息是使用這些參數呼叫 [`finalizeDeposit` 函式](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol#L141-L148): + +| 參數 | 數值 | 意義 | +| ------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| \_l1Token | address(0) | 用於代表 L1 上 ETH (它不是 ERC-20 代幣) 的特殊值 | +| \_l2Token | Lib_PredeployAddresses.OVM_ETH | 在 Optimism 上管理 ETH 的 L2 合約,`0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000` (此合約僅供 Optimism 內部使用) | +| \_from | \_from | 在 L1 上傳送 ETH 的地址 | +| \_to | \_to | 在 L2 上接收 ETH 的地址 | +| 數量 | msg.value | 傳送的 wei 數量 (已經傳送到跨鏈橋) | +| \_data | \_data | 附加到存款的額外資料 | + +```solidity + // 將 calldata 傳送到 L2 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l2TokenBridge, _l2Gas, message); +``` + +透過跨網域信使傳送訊息。 + +```solidity + // slither-disable-next-line reentrancy-events + emit ETHDepositInitiated(_from, _to, msg.value, _data); + } +``` + +發出一個事件,以通知任何監聽此轉移的去中心化應用程式。 + +```solidity + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20( + . + . + . + ) external virtual onlyEOA { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data); + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20To( + . + . + . + ) external virtual { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data); + } +``` + +這兩個函式是 `_initiateERC20Deposit` 的包裝器,後者處理實際的 ERC-20 存款。 + +```solidity + /** + * @dev 透過通知 L2 存款代幣合約存款並呼叫處理程序來鎖定 L1 資金 (例如,transferFrom) 來執行存款的邏輯。 + * + * @param _l1Token 我們正在存入的 L1 ERC20 的地址 + * @param _l2Token L1 各自在 L2 的 ERC20 地址 + * @param _from 從 L1 提取存款的帳戶 + * @param _to 在 L2 上給予存款的帳戶 + * @param _amount 要存入的 ERC20 數量。 + * @param _l2Gas 在 L2 上完成存款所需的 Gas 限制。 + * @param _data 可選資料,轉發到 L2。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function _initiateERC20Deposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) internal { +``` + +這個函式與上面的 `_initiateETHDeposit` 相似,但有幾個重要的差異。 +第一個差異是此函式接收代幣地址和要轉移的金額作為參數。 +在 ETH 的情況下,對跨鏈橋的呼叫已經包含了將資產轉移到跨鏈橋帳戶 (`msg.value`)。 + +```solidity + // 當存款在 L1 上啟動時,L1 跨鏈橋會將資金轉移給自己以供未來提款。safeTransferFrom 也會檢查合約是否有程式碼, + // 所以如果 _from 是 EOA 或 address(0),這將會失敗。 + // slither-disable-next-line reentrancy-events, reentrancy-benign + IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); +``` + +ERC-20 代幣轉移遵循與 ETH 不同的流程: + +1. 使用者 (`_from`) 給予跨鏈橋一筆額度以轉移適當的代幣。 +2. 使用者以代幣合約地址、金額等呼叫跨鏈橋。 +3. 跨鏈橋作為存款過程的一部分,將代幣轉移 (給自己)。 + +第一步可能與後兩步在不同的交易中發生。 +然而,搶先交易不是問題,因為呼叫 `_initiateERC20Deposit` 的兩個函式 (`depositERC20` 和 `depositERC20To`) 只會以 `msg.sender` 作為 `_from` 參數來呼叫此函式。 + +```solidity + // 建構 _l2Token.finalizeDeposit(_to, _amount) 的 calldata + bytes memory message = abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + _l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + + // 將 calldata 傳送到 L2 + // slither-disable-next-line reentrancy-events, reentrancy-benign + sendCrossDomainMessage(l2TokenBridge, _l2Gas, message); + + // slither-disable-next-line reentrancy-benign + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; +``` + +將存入的代幣數量新增到 `deposits` 資料結構中。 +在 L2 上可能有多個地址對應到同一個 L1 ERC-20 代幣,因此僅使用跨鏈橋的 L1 ERC-20 代幣餘額來追蹤存款是不夠的。 + +```solidity + + // slither-disable-next-line reentrancy-events + emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /************************* + * 跨鏈函式 * + *************************/ + + /** + * @inheritdoc IL1StandardBridge + */ + function finalizeETHWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _data +``` + +L2 跨鏈橋向 L2 跨網域信使傳送一則訊息,這會導致 L1 跨網域信使呼叫此函式 (當然,前提是在 L1 上提交了[最終確定該訊息的交易](https://community.optimism.io/docs/developers/bridge/messaging/#fees-for-l2-%E2%87%92-l1-transactions))。 + +```solidity + ) external onlyFromCrossDomainAccount(l2TokenBridge) { +``` + +確保這是一則_合法_的訊息,來自跨網域信使,並源自 L2 代幣跨鏈橋。 +此函式用於從跨鏈橋提取 ETH,因此我們必須確保它只由授權的呼叫者呼叫。 + +```solidity + // slither-disable-next-line reentrancy-events + (bool success, ) = _to.call{ value: _amount }(new bytes(0)); +``` + +轉移 ETH 的方法是呼叫收款人,並在 `msg.value` 中附上 wei 的數量。 + +```solidity + require(success, "TransferHelper::safeTransferETH: ETH transfer failed"); + + // slither-disable-next-line reentrancy-events + emit ETHWithdrawalFinalized(_from, _to, _amount, _data); +``` + +發出一個關於提款的事件。 + +```solidity + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external onlyFromCrossDomainAccount(l2TokenBridge) { +``` + +這個函式與上面的 `finalizeETHWithdrawal` 相似,但針對 ERC-20 代幣做了必要的修改。 + +```solidity + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount; +``` + +更新 `deposits` 資料結構。 + +```solidity + + // 當提款在 L1 上最終確定時,L1 跨鏈橋會將資金轉移給提款人 + // slither-disable-next-line reentrancy-events + IERC20(_l1Token).safeTransfer(_to, _amount); + + // slither-disable-next-line reentrancy-events + emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + + /***************************** + * 臨時 - 遷移 ETH * + *****************************/ + + /** + * @dev 將 ETH 餘額新增到帳戶。這是為了允許將 ETH + * 從舊的閘道遷移到新的閘道。 + * 注意:這只保留一次升級,以便我們能夠從舊合約接收遷移的 ETH + */ + function donateETH() external payable {} +} +``` + +之前有一個較早的跨鏈橋實作。 +當我們從那個實作轉移到這個實作時,我們必須移動所有資產。 +ERC-20 代幣可以直接移動。 +然而,要將 ETH 轉移到一個合約,你需要該合約的批准,這正是 `donateETH` 提供給我們的。 + +## L2 上的 ERC-20 代幣 {#erc-20-tokens-on-l2} + +要讓一個 ERC-20 代幣適用於標準跨鏈橋,它需要允許標準跨鏈橋,且_只_允許標準跨鏈橋,來鑄造代幣。 +這是必要的,因為跨鏈橋需要確保在 Optimism 上流通的代幣數量等於鎖在 L1 跨鏈橋合約內的代幣數量。 +如果 L2 上的代幣太多,一些使用者將無法將他們的資產橋接回 L1。 +這樣一來,我們實質上會重新創造出[部分準備金銀行制度](https://www.investopedia.com/terms/f/fractionalreservebanking.asp),而不是一個可信的跨鏈橋。 +如果 L1 上的代幣太多,其中一些代幣將永遠被鎖在跨鏈橋合約內,因為沒有辦法在不銷毀 L2 代幣的情況下釋放它們。 + +### IL2StandardERC20 {#il2standarderc20} + +在 L2 上使用標準跨鏈橋的每個 ERC-20 代幣都需要提供[這個介面](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/standards/IL2StandardERC20.sol),其中包含標準跨鏈橋所需的函式和事件。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +``` + +[標準 ERC-20 介面](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) 不包含 `mint` 和 `burn` 函式。 +這些方法並非 [ERC-20 標準](https://eips.ethereum.org/EIPS/eip-20)所要求的,該標準未指定建立和銷毀代幣的機制。 + +```solidity +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +``` + +[ERC-165 介面](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/introspection/IERC165.sol) 用於指定合約提供的函式。 +[您可以在此處閱讀該標準](https://eips.ethereum.org/EIPS/eip-165)。 + +```solidity +interface IL2StandardERC20 is IERC20, IERC165 { + function l1Token() external returns (address); +``` + +此函式提供橋接到此合約的 L1 代幣的地址。 +請注意,我們沒有一個反向的類似函式。 +我們需要能夠橋接任何 L1 代幣,無論在其實作時是否已規劃 L2 支援。 + +```solidity + + function mint(address _to, uint256 _amount) external; + + function burn(address _from, uint256 _amount) external; + + event Mint(address indexed _account, uint256 _amount); + event Burn(address indexed _account, uint256 _amount); +} +``` + +鑄造 (建立) 和銷毀 (摧毀) 代幣的函式和事件。 +跨鏈橋應該是唯一可以執行這些函式的實體,以確保代幣數量是正確的 (等於鎖在 L1 上的代幣數量)。 + +### L2StandardERC20 {#L2StandardERC20} + +[這是我們對 `IL2StandardERC20` 介面的實作](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/standards/L2StandardERC20.sol)。 +除非您需要某種自訂邏輯,否則您應該使用這個。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +``` + +[OpenZeppelin ERC-20 合約](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol)。 +Optimism 不相信重新發明輪子,尤其是當這個輪子經過良好審核且需要足夠可信以持有資產時。 + +```solidity +import "./IL2StandardERC20.sol"; + +contract L2StandardERC20 is IL2StandardERC20, ERC20 { + address public l1Token; + address public l2Bridge; +``` + +這是我們需要而 ERC-20 通常不需要的兩個額外設定參數。 + +```solidity + + /** + * @param _l2Bridge L2 標準跨鏈橋的地址。 + * @param _l1Token 相應 L1 代幣的地址。 + * @param _name ERC20 名稱。 + * @param _symbol ERC20 符號。 + */ + constructor( + address _l2Bridge, + address _l1Token, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + l1Token = _l1Token; + l2Bridge = _l2Bridge; + } +``` + +首先呼叫我們繼承的合約的建構函式 (`ERC20(_name, _symbol)`),然後設定我們自己的變數。 + +```solidity + + modifier onlyL2Bridge() { + require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); + _; + } + + + // slither-disable-next-line external-function + function supportsInterface(bytes4 _interfaceId) public pure returns (bool) { + bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 + bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^ + IL2StandardERC20.mint.selector ^ + IL2StandardERC20.burn.selector; + return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface; + } +``` + +這就是 [ERC-165](https://eips.ethereum.org/EIPS/eip-165) 的運作方式。 +每個介面都有一系列支援的函式,並被識別為這些函式的[ABI 函式選擇器](https://docs.soliditylang.org/en/v0.8.12/abi-spec.html#function-selector)的[互斥或](https://en.wikipedia.org/wiki/Exclusive_or)。 + +L2 跨鏈橋使用 ERC-165 作為健全性檢查,以確保其傳送資產的 ERC-20 合約是一個 `IL2StandardERC20`。 + +**注意:** 沒有任何東西可以阻止惡意合約對 `supportsInterface` 提供虛假答案,所以這是一個健全性檢查機制,_不是_一個安全機制。 + +```solidity + // slither-disable-next-line external-function + function mint(address _to, uint256 _amount) public virtual onlyL2Bridge { + _mint(_to, _amount); + + emit Mint(_to, _amount); + } + + // slither-disable-next-line external-function + function burn(address _from, uint256 _amount) public virtual onlyL2Bridge { + _burn(_from, _amount); + + emit Burn(_from, _amount); + } +} +``` + +只有 L2 跨鏈橋被允許鑄造和銷毀資產。 + +`_mint` 和 `_burn` 實際上是在 [OpenZeppelin ERC-20 合約](/developers/tutorials/erc20-annotated-code/#the-_mint-and-_burn-functions-_mint-and-_burn) 中定義的。 +該合約只是沒有將它們公開,因為鑄造和銷毀代幣的條件與使用 ERC-20 的方式一樣多種多樣。 + +## L2 跨鏈橋程式碼 {#l2-bridge-code} + +這是執行在 Optimism 上的跨鏈橋程式碼。 +[此合約的原始碼在此](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol)。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/* 介面匯入 */ +import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol"; +import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol"; +import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol"; +``` + +[IL2ERC20Bridge](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol) 介面與我們上面看到的 [L1 對應版本](#IL1ERC20Bridge) 非常相似。 +有兩個顯著的差異: + +1. 在 L1 上,您啟動存款並最終確定提款。 + 在這裡,您啟動提款並最終確定存款。 +2. 在 L1 上,有必要區分 ETH 和 ERC-20 代幣。 + 在 L2 上,我們可以對兩者使用相同的函式,因為在 Optimism 內部,ETH 餘額被當作一個地址為 [0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000](https://explorer.optimism.io/address/0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000) 的 ERC-20 代幣來處理。 + +```solidity +/* 函式庫匯入 */ +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol"; +import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol"; + +/* 合約匯入 */ +import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol"; + +/** + * @title L2StandardBridge + * @dev L2 標準跨鏈橋是一個與 L1 標準跨鏈橋協同工作的合約, + * 用於在 L1 和 L2 之間進行 ETH 和 ERC20 的轉換。 + * 當此合約聽到 L1 標準跨鏈橋有存款時,它會作為新代幣的鑄造者。 + * 此合約也作為預計提款代幣的銷毀者,通知 L1 跨鏈橋釋放 L1 資金。 + */ +contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled { + /******************************** + * 外部合約參考 * + ********************************/ + + address public l1TokenBridge; +``` + +追蹤 L1 跨鏈橋的地址。 +請注意,與 L1 的對應版本相反,我們在這裡_需要_這個變數。 +L1 跨鏈橋的地址不是預先知道的。 + +```solidity + + /*************** + * 建構函式 * + ***************/ + + /** + * @param _l2CrossDomainMessenger 此合約使用的跨網域信使。 + * @param _l1TokenBridge 部署在主鏈上的 L1 跨鏈橋地址。 + */ + constructor(address _l2CrossDomainMessenger, address _l1TokenBridge) + CrossDomainEnabled(_l2CrossDomainMessenger) + { + l1TokenBridge = _l1TokenBridge; + } + + /*************** + * 提款 * + ***************/ + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdraw( + address _l2Token, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) external virtual { + _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data); + } + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) external virtual { + _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data); + } +``` + +這兩個函式啟動提款。 +請注意,無需指定 L1 代幣地址。 +L2 代幣應該會告訴我們 L1 對應的地址。 + +```solidity + + /** + * @dev 透過銷毀代幣並通知 L1 代幣閘道提款來執行提款的邏輯。 + * @param _l2Token 提款啟動的 L2 代幣地址。 + * @param _from 從 L2 提取提款的帳戶。 + * @param _to 在 L1 上給予提款的帳戶。 + * @param _amount 要提取的代幣數量。 + * @param _l1Gas 未使用,但為潛在的前向相容性考量而包含。 + * @param _data 可選資料,轉發到 L1。此資料僅為方便外部合約而提供。 + * 除了強制執行最大長度外,這些合約對其內容不提供任何保證。 + */ + function _initiateWithdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) internal { + // 當提款啟動時,我們銷毀提款人的資金以防止後續的 L2 使用 + // slither-disable-next-line reentrancy-events + IL2StandardERC20(_l2Token).burn(msg.sender, _amount); +``` + +請注意,我們_不_依賴 `_from` 參數,而是依賴 `msg.sender`,後者更難偽造 (就我所知,是不可能的)。 + +```solidity + + // 建構 l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) 的 calldata + // slither-disable-next-line reentrancy-events + address l1Token = IL2StandardERC20(_l2Token).l1Token(); + bytes memory message; + + if (_l2Token == Lib_PredeployAddresses.OVM_ETH) { +``` + +在 L1 上有必要區分 ETH 和 ERC-20。 + +```solidity + message = abi.encodeWithSelector( + IL1StandardBridge.finalizeETHWithdrawal.selector, + _from, + _to, + _amount, + _data + ); + } else { + message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + } + + // 將訊息傳送到 L1 跨鏈橋 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l1TokenBridge, _l1Gas, message); + + // slither-disable-next-line reentrancy-events + emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + } + + /************************************ + * 跨鏈函式:存款 * + ************************************/ + + /** + * @inheritdoc IL2ERC20Bridge + */ + function finalizeDeposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data +``` + +此函式由 `L1StandardBridge` 呼叫。 + +```solidity + ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) { +``` + +確保訊息的來源是合法的。 +這很重要,因為此函式會呼叫 `_mint`,且可能被用來發放不受 L1 跨鏈橋所擁有代幣覆蓋的代幣。 + +```solidity + // 檢查目標代幣是否合規,並驗證 L1 上存入的代幣與此處的 L2 存款代幣表示相符 + if ( + // slither-disable-next-line reentrancy-events + ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && + _l1Token == IL2StandardERC20(_l2Token).l1Token() +``` + +健全性檢查: + +1. 支援正確的介面 +2. L2 ERC-20 合約的 L1 地址與代幣的 L1 來源相符 + +```solidity + ) { + // 當存款最終確定時,我們在 L2 上將相同數量的代幣記入帳戶。 + // slither-disable-next-line reentrancy-events + IL2StandardERC20(_l2Token).mint(_to, _amount); + // slither-disable-next-line reentrancy-events + emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); +``` + +如果健全性檢查通過,則最終確定存款: + +1. 鑄造代幣 +2. 發出適當的事件 + +```solidity + } else { + // 存入的 L2 代幣對其 L1 代幣的正確地址有異議,或者不支援正確的介面。 + // 這只應該在有惡意的 L2 代幣,或者使用者以某種方式指定了錯誤的 L2 代幣地址存入時發生。 + // 無論哪種情況,我們都在此處停止流程並建構一個提款訊息, + // 以便使用者在某些情況下可以取回他們的資金。 + // 完全防止惡意代幣合約是不可能的,但這確實限制了使用者錯誤並減輕了某些形式的惡意合約行為。 +``` + +如果使用者因為使用錯誤的 L2 代幣地址而犯了一個可被偵測的錯誤,我們希望取消存款並在 L1 上歸還代幣。 +我們能從 L2 做到這一點的唯一方法是傳送一則需要等待錯誤挑戰期的訊息,但對使用者來說,這比永久失去代幣要好得多。 + +```solidity + bytes memory message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + _l1Token, + _l2Token, + _to, // 在這裡交換了 _to 和 _from 以將存款退回給傳送者 + _from, + _amount, + _data + ); + + // 將訊息傳送到 L1 跨鏈橋 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l1TokenBridge, 0, message); + // slither-disable-next-line reentrancy-events + emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data); + } + } +} +``` + +## 結論 {#conclusion} + +標準跨鏈橋是資產轉移最靈活的機制。 +然而,正因為它如此通用,它不總是最好用的機制。 +特別是對於提款,大多數使用者更喜歡使用[第三方跨鏈橋](https://optimism.io/apps#bridge),這些跨鏈橋不等待挑戰期,也不需要梅克爾證明來最終確定提款。 + +這些跨鏈橋通常透過在 L1 上持有資產來運作,它們會立即提供資產並收取少量費用 (通常低於標準跨鏈橋提款的 Gas 成本)。 +當跨鏈橋 (或其運營者) 預期 L1 資產不足時,它會從 L2 轉移足夠的資產。 由於這些都是非常大的提款,提款成本會攤銷到大量金額上,因此所佔的百分比要小得多。 + +希望這篇文章能幫助您更了解第二層如何運作,以及如何撰寫清晰且安全的 Solidity 程式碼。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/reverse-engineering-a-contract/index.md b/public/content/translations/zh-tw/developers/tutorials/reverse-engineering-a-contract/index.md new file mode 100644 index 00000000000..3edb01dde99 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/reverse-engineering-a-contract/index.md @@ -0,0 +1,743 @@ +--- +title: "對合約進行逆向工程" +description: "如何在沒有原始碼的情況下理解合約" +author: Ori Pomerantz +lang: zh-tw +tags: [ "evm", "opcodes" ] +skill: advanced +published: 2021-12-30 +--- + +## 介紹 {#introduction} + +_區塊鏈上沒有秘密_,發生的一切都是一致、可驗證且公開的。 理想情況下,[合約應在 Etherscan 上發布並驗證其原始碼](https://etherscan.io/address/0xb8901acb165ed027e32754e0ffe830802919727f#code)。 然而,[情況並非總是如此](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#code)。 在本文中,您將學習如何透過查看一個沒有原始碼的合約 [`0x2510c039cc3b061d79e564b38836da87e31b342f`](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f) 來對其進行逆向工程。 + +有反向編譯器,但它們並不總能產生[可用的結果](https://etherscan.io/bytecode-decompiler?a=0x2510c039cc3b061d79e564b38836da87e31b342f)。 在本文中,您將學習如何手動從 [opcodes](https://github.com/wolflo/evm-opcodes) 逆向工程並理解一個合約,以及如何解釋反編譯器的結果。 + +要理解本文,您應該已經了解 EVM 的基礎知識,並至少對 EVM 組譯器有些熟悉。 [您可以在此處閱讀有關這些主題的資訊](https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e)。 + +## 準備可執行程式碼 {#prepare-the-executable-code} + +您可以前往合約的 Etherscan 頁面,點擊 **Contract** 標籤,然後點擊 **Switch to Opcodes View** 來取得 opcodes。 您將得到一個每行一個 opcode 的視圖。 + +![來自 Etherscan 的 Opcode 視圖](opcode-view.png) + +然而,為了能夠理解跳轉,您需要知道每個 opcode 在程式碼中的位置。 要做到這一點,一種方法是打開一個 Google 試算表並將 opcodes 貼到 C 欄。[您可以透過建立這個已準備好的試算表的副本來跳過以下步驟](https://docs.google.com/spreadsheets/d/1tKmTJiNjUwHbW64wCKOSJxHjmh0bAUapt6btUYE7kDA/edit?usp=sharing)。 + +下一步是取得正確的程式碼位置,以便我們能夠理解跳轉。 我們將 opcode 大小放在 B 欄,位置(十六進位)放在 A 欄。在儲存格 `B1` 中輸入此函數,然後複製並貼到 B 欄的其餘部分,直到程式碼結束。 完成此操作後,您可以隱藏 B 欄。 + +``` +=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0) +``` + +首先,此函數為 opcode 本身增加一個位元組,然後尋找 `PUSH`。 Push opcodes 很特殊,因為它們需要額外的位元組來儲存被推送的值。 如果 opcode 是 `PUSH`,我們就提取位元組數並將其加上。 + +在 `A1` 中放入第一個位移量,零。 然後,在 `A2` 中放入此函數,並再次複製貼到 A 欄的其餘部分: + +``` +=dec2hex(hex2dec(A1)+B1) +``` + +我們需要此函數來提供十六進位值,因為在跳轉(`JUMP` 和 `JUMPI`)之前推送的值是以十六進位形式給我們的。 + +## 進入點 (0x00) {#the-entry-point-0x00} + +合約總是從第一個位元組開始執行。 這是程式碼的初始部分: + +| 位移 | Opcode | 堆疊(在 opcode 之後) | +| -: | ------------ | ---------------------------------------------- | +| 0 | PUSH1 0x80 | 0x80 | +| 2 | PUSH1 0x40 | 0x40, 0x80 | +| 4 | MSTORE | 空的 | +| 5 | PUSH1 0x04 | 0x04 | +| 7 | CALLDATASIZE | CALLDATASIZE 0x04 | +| 8 | LT | CALLDATASIZE\<4 | +| 9 | PUSH2 0x005e | 0x5E CALLDATASIZE\<4 | +| C | JUMPI | 空的 | + +這段程式碼做了兩件事: + +1. 將 0x80 作為一個 32 位元組的值寫入記憶體位置 0x40-0x5F(0x80 儲存在 0x5F,而 0x40-0x5E 都是零)。 +2. 讀取 calldata 大小。 通常,以太坊合約的呼叫資料遵循[應用程式二進位介面 (ABI)](https://docs.soliditylang.org/en/v0.8.10/abi-spec.html),該介面至少需要四個位元組用於函數選擇器。 如果呼叫資料大小小於四,跳轉到 0x5E。 + +![這部分的流程圖](flowchart-entry.png) + +### 0x5E 的處理常式(用於非 ABI 呼叫資料) {#the-handler-at-0x5e-for-non-abi-call-data} + +| 位移 | Opcode | +| -: | ------------ | +| 5E | JUMPDEST | +| 5E | CALLDATASIZE | +| 60 | PUSH2 0x007c | +| 63 | JUMPI | + +此程式碼片段以 `JUMPDEST` 開頭。 EVM(以太坊虛擬機)程式如果跳轉到不是 `JUMPDEST` 的 opcode,將會拋出例外。 然後它會查看 CALLDATASIZE,如果它是「真」(即,非零),則跳轉到 0x7C。 我們稍後會講到這個。 + +| 位移 | Opcode | 堆疊(在 opcode 之後) | +| -: | ---------- | -------------------------------------------------------------------------------------- | +| 64 | CALLVALUE | 呼叫提供的 [Wei](/glossary/#wei)。 在 Solidity 中稱為 `msg.value` | +| 65 | PUSH1 0x06 | 6 CALLVALUE | +| 67 | PUSH1 0x00 | 0 6 CALLVALUE | +| 69 | DUP3 | CALLVALUE 0 6 CALLVALUE | +| 6A | DUP3 | 6 CALLVALUE 0 6 CALLVALUE | +| 6B | SLOAD | Storage[6] CALLVALUE 0 6 CALLVALUE | + +所以當沒有呼叫資料時,我們讀取 Storage[6] 的值。 我們還不知道這個值是什麼,但我們可以尋找合約收到的沒有呼叫資料的交易。 在 Etherscan 中,僅轉帳 ETH 而沒有任何呼叫資料(因此沒有方法)的交易,其方法為 `Transfer`。 事實上,[合約收到的第一筆交易](https://etherscan.io/tx/0xeec75287a583c36bcc7ca87685ab41603494516a0f5986d18de96c8e630762e7) 就是一筆轉帳。 + +如果我們查看那筆交易並點擊 **Click to see More**,我們會看到呼叫資料(稱為輸入資料)確實是空的(`0x`)。 另請注意,其價值為 1.559 ETH,這在稍後會相關。 + +![呼叫資料為空](calldata-empty.png) + +接下來,點擊 **State** 標籤並展開我們正在進行逆向工程的合約 (0x2510...)。 您可以看到 `Storage[6]` 在交易期間確實發生了變化,如果您將 Hex 改為 **Number**,您會看到它變成了 1,559,000,000,000,000,000,即以 wei 為單位的轉帳值(為清晰起見,我加了逗號),對應於下一個合約值。 + +![Storage[6] 的變化](storage6.png) + +如果我們查看由[同一時期的其他 `Transfer` 交易](https://etherscan.io/tx/0xf708d306de39c422472f43cb975d97b66fd5d6a6863db627067167cbf93d84d1#statechange)引起的狀態變更,我們可以看到 `Storage[6]` 在一段時間內追蹤了合約的價值。 目前我們將其稱為 `Value*`。 星號 (`*`) 提醒我們,我們還不_知道_這個變數的作用,但它不可能只是為了追蹤合約價值,因為當您可以使用 `ADDRESS BALANCE` 取得帳戶餘額時,沒有必要使用非常昂貴的儲存空間。 第一個 opcode 推送合約自己的地址。 第二個 opcode 讀取堆疊頂部的地址,並將其替換為該地址的餘額。 + +| 位移 | Opcode | 堆疊 | +| -: | ------------ | ------------------------------------------- | +| 6C | PUSH2 0x0075 | 0x75 Value\* CALLVALUE 0 6 CALLVALUE | +| 6F | SWAP2 | CALLVALUE Value\* 0x75 0 6 CALLVALUE | +| 70 | SWAP1 | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 71 | PUSH2 0x01a7 | 0x01A7 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 74 | JUMP | | + +我們將在跳轉目的地繼續追蹤這段程式碼。 + +| 位移 | Opcode | 堆疊 | +| --: | ---------- | ----------------------------------------------------------- | +| 1A7 | JUMPDEST | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1A8 | PUSH1 0x00 | 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AA | DUP3 | CALLVALUE 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AB | NOT | 2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | + +`NOT` 是位元運算,所以它會反轉呼叫值中每個位元的值。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ------------------------------------------------------------------------------------------------------ | +| 1AC | DUP3 | Value\* 2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AD | GT | Value\*>2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AE | ISZERO | Value\*\<=2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AF | PUSH2 0x01df | 0x01DF Value\*\<=2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1B2 | JUMPI | | + +如果 `Value*` 小於或等於 2^256-CALLVALUE-1,我們就跳轉。 這看起來像是防止溢位的邏輯。 事實上,我們看到在位移 0x01DE 處,經過一些無意義的操作後(例如,寫入即將被刪除的記憶體),如果檢測到溢位,合約會還原,這是正常行為。 + +請注意,這樣的溢位極不可能發生,因為它需要呼叫值加上 `Value*` 的總和與 2^256 wei(約 10^59 ETH)相當。 [在撰寫本文時,ETH 的總供應量不到兩億](https://etherscan.io/stat/supply)。 + +| 位移 | Opcode | 堆疊 | +| --: | -------- | ----------------------------------------- | +| 1DF | JUMPDEST | 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1E0 | POP | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1E1 | ADD | Value\*+CALLVALUE 0x75 0 6 CALLVALUE | +| 1E2 | SWAP1 | 0x75 Value\*+CALLVALUE 0 6 CALLVALUE | +| 1E3 | JUMP | | + +如果我們到達這裡,取得 `Value* + CALLVALUE` 並跳轉到位移 0x75。 + +| 位移 | Opcode | 堆疊 | +| -: | -------- | ------------------------------- | +| 75 | JUMPDEST | Value\*+CALLVALUE 0 6 CALLVALUE | +| 76 | SWAP1 | 0 Value\*+CALLVALUE 6 CALLVALUE | +| 77 | SWAP2 | 6 Value\*+CALLVALUE 0 CALLVALUE | +| 78 | SSTORE | 0 CALLVALUE | + +如果我們到達這裡(這要求呼叫資料為空),我們會將呼叫值加到 `Value*` 上。 這與我們所說的 `Transfer` 交易的作用一致。 + +| 位移 | Opcode | +| -: | ------ | +| 79 | POP | +| 7A | POP | +| 7B | STOP | + +最後,清除堆疊(這不是必要的)並標示交易成功結束。 + +總結一下,這是初始程式碼的流程圖。 + +![進入點流程圖](flowchart-entry.png) + +## 0x7C 的處理常式 {#the-handler-at-0x7c} + +我故意不在標題中說明這個處理常式的作用。 重點不是教您這個特定合約如何運作,而是如何對合約進行逆向工程。 您將透過追蹤程式碼,以與我相同的方式了解它的作用。 + +我們從幾個地方來到這裡: + +- 如果呼叫資料為 1、2 或 3 個位元組(從位移 0x63 開始) +- 如果方法簽名未知(從位移 0x42 和 0x5D 開始) + +| 位移 | Opcode | 堆疊 | +| -: | ------------ | ------------------------------------------------------------------------ | +| 7C | JUMPDEST | | +| 7D | PUSH1 0x00 | 0x00 | +| 7F | PUSH2 0x009d | 0x9D 0x00 | +| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 | +| 84 | SLOAD | Storage[3] 0x9D 0x00 | + +這是另一個儲存單元,我在任何交易中都找不到它,所以很難知道它的意思。 下面的程式碼將使其更清晰。 + +| 位移 | Opcode | 堆疊 | +| -: | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 | +| 9A | AND | Storage[3]-as-address 0x9D 0x00 | + +這些 opcodes 將我們從 Storage[3] 讀取的值截斷為 160 位元,即以太坊地址的長度。 + +| 位移 | Opcode | 堆疊 | +| -: | ------ | ----------------------------------------------------------------------------------- | +| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 | +| 9C | JUMP | Storage[3]-as-address 0x00 | + +這個跳轉是多餘的,因為我們要去下一個 opcode。 這段程式碼的 gas 效率遠不及應有的水平。 + +| 位移 | Opcode | 堆疊 | +| -: | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| 9D | JUMPDEST | Storage[3]-as-address 0x00 | +| 9E | SWAP1 | 0x00 Storage[3]-as-address | +| 9F | POP | Storage[3]-as-address | +| A0 | PUSH1 0x40 | 0x40 Storage[3]-as-address | +| A2 | MLOAD | Mem[0x40] Storage[3]-as-address | + +在程式碼的最開頭,我們將 Mem[0x40] 設置為 0x80。 如果我們稍後查看 0x40,我們會發現我們沒有改變它——所以我們可以假設它是 0x80。 + +| 位移 | Opcode | 堆疊 | +| -: | ------------ | ----------------------------------------------------------------------------------------------------- | +| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-as-address | +| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-as-address | +| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address | +| A7 | CALLDATACOPY | 0x80 Storage[3]-as-address | + +將所有呼叫資料複製到記憶體,從 0x80 開始。 + +| 位移 | Opcode | 堆疊 | +| -: | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-as-address | +| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-as-address | +| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AD | DUP6 | Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AE | GAS | GAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AF | DELEGATE_CALL | | + +現在事情清楚多了。 這個合約可以作為[代理](https://blog.openzeppelin.com/proxy-patterns/),呼叫 Storage[3] 中的地址來完成實際工作。 `DELEGATE_CALL` 呼叫一個單獨的合約,但停留在同一個儲存空間。 這意味著被代理的合約,即我們作為代理的合約,會存取相同的儲存空間。 呼叫的參數是: + +- _Gas_:所有剩餘的 gas +- _被呼叫的地址_:Storage[3]-as-address +- _呼叫資料_:從 0x80 開始的 CALLDATASIZE 位元組,這是我們放置原始呼叫資料的地方 +- _返回資料_:無 (0x00 - 0x00) 我們將透過其他方式取得返回資料(見下文) + +| 位移 | Opcode | 堆疊 | +| -: | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B0 | RETURNDATASIZE | RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B5 | RETURNDATACOPY | RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | + +在這裡,我們將所有返回資料複製到從 0x80 開始的記憶體緩衝區。 + +| 位移 | Opcode | 堆疊 | +| -: | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B6 | DUP2 | (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B7 | DUP1 | (((呼叫成功/失敗))) (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B8 | ISZERO | (((呼叫失敗了嗎))) (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| B9 | PUSH2 0x00c0 | 0xC0 (((呼叫失敗了嗎))) (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| BC | JUMPI | (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| BD | DUP2 | RETURNDATASIZE (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| BE | DUP5 | 0x80 RETURNDATASIZE (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| BF | RETURN | | + +因此,在呼叫之後,我們將返回資料複製到緩衝區 0x80 - 0x80+RETURNDATASIZE,如果呼叫成功,我們就用那個緩衝區進行 `RETURN`。 + +### DELEGATECALL 失敗 {#delegatecall-failed} + +如果我們到達這裡,到 0xC0,這意味著我們呼叫的合約已還原。 由於我們只是該合約的代理,我們希望返回相同的資料並也進行還原。 + +| 位移 | Opcode | 堆疊 | +| -: | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| C0 | JUMPDEST | (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| C1 | DUP2 | RETURNDATASIZE (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| C2 | DUP5 | 0x80 RETURNDATASIZE (((呼叫成功/失敗))) RETURNDATASIZE (((呼叫成功/失敗))) 0x80 Storage[3]-as-address | +| C3 | REVERT | | + +所以我們使用與之前用於 `RETURN` 相同的緩衝區進行 `REVERT`:0x80 - 0x80+RETURNDATASIZE + +![代理呼叫流程圖](flowchart-proxy.png) + +## ABI 呼叫 {#abi-calls} + +如果呼叫資料大小為四個位元組或更多,這可能是一個有效的 ABI 呼叫。 + +| 位移 | Opcode | 堆疊 | +| -: | ------------ | -------------------------------------------------------------------------------------------------------- | +| D | PUSH1 0x00 | 0x00 | +| F | CALLDATALOAD | (((呼叫資料的第一個字 (256 位元))) | +| 10 | PUSH1 0xe0 | 0xE0 (((呼叫資料的第一個字 (256 位元))) | +| 12 | SHR | (((呼叫資料的前 32 位元 (4 位元組))) | + +Etherscan 告訴我們 `1C` 是一個未知的 opcode,因為[它是在 Etherscan 編寫此功能後添加的](https://eips.ethereum.org/EIPS/eip-145),而且他們尚未更新。 一份[最新的 opcode 表格](https://github.com/wolflo/evm-opcodes)顯示這是向右移位 + +| 位移 | Opcode | 堆疊 | +| -: | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 13 | DUP1 | (((呼叫資料的前 32 位元 (4 位元組))) (((呼叫資料的前 32 位元 (4 位元組))) | +| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((呼叫資料的前 32 位元 (4 位元組))) (((呼叫資料的前 32 位元 (4 位元組))) | +| 19 | GT | 0x3CD8045E>呼叫資料的前 32 位元 (((呼叫資料的前 32 位元 (4 位元組))) | +| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>呼叫資料的前 32 位元 (((呼叫資料的前 32 位元 (4 位元組))) | +| 1D | JUMPI | (((呼叫資料的前 32 位元 (4 位元組))) | + +像這樣將方法簽名匹配測試分成兩部分,平均可以節省一半的測試時間。 緊隨其後的程式碼和 0x43 中的程式碼遵循相同的模式:`DUP1` 呼叫資料的前 32 位元,`PUSH4 (((方法簽名)))`,執行 `EQ` 檢查是否相等,然後如果方法簽名匹配則 `JUMPI`。 以下是方法簽名、它們的地址,以及(如果已知)[相應的方法定義](https://www.4byte.directory/): + +| 方法 | 方法簽名 | 跳轉到的位移 | +| --------------------------------------------------------------------------------------------------------- | ---------- | ------ | +| [splitter()](https://www.4byte.directory/signatures/?bytes4_signature=0x3cd8045e) | 0x3cd8045e | 0x0103 | +| ??? | 0x81e580d3 | 0x0138 | +| [currentWindow()](https://www.4byte.directory/signatures/?bytes4_signature=0xba0bafb4) | 0xba0bafb4 | 0x0158 | +| ??? | 0x1f135823 | 0x00C4 | +| [merkleRoot()](https://www.4byte.directory/signatures/?bytes4_signature=0x2eb4a7ab) | 0x2eb4a7ab | 0x00ED | + +如果找不到匹配項,程式碼會跳轉到 [0x7C 的代理處理常式](#the-handler-at-0x7c),希望我們作為代理的合約有匹配項。 + +![ABI 呼叫流程圖](flowchart-abi.png) + +## splitter() {#splitter} + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ----------------------------- | +| 103 | JUMPDEST | | +| 104 | CALLVALUE | CALLVALUE | +| 105 | DUP1 | CALLVALUE CALLVALUE | +| 106 | ISZERO | CALLVALUE==0 CALLVALUE | +| 107 | PUSH2 0x010f | 0x010F CALLVALUE==0 CALLVALUE | +| 10A | JUMPI | CALLVALUE | +| 10B | PUSH1 0x00 | 0x00 CALLVALUE | +| 10D | DUP1 | 0x00 0x00 CALLVALUE | +| 10E | REVERT | | + +這個函數首先檢查呼叫是否發送了任何 ETH。 這個函數不是 [`payable`](https://solidity-by-example.org/payable/)。 如果有人向我們發送 ETH,那一定是個錯誤,我們希望 `REVERT` 以避免那些 ETH 留在他們無法取回的地方。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 10F | JUMPDEST | | +| 110 | POP | | +| 111 | PUSH1 0x03 | 0x03 | +| 113 | SLOAD | (((Storage[3] 又名我們代理的合約))) | +| 114 | PUSH1 0x40 | 0x40 (((Storage[3] 又名我們代理的合約))) | +| 116 | MLOAD | 0x80 (((Storage[3] 又名我們代理的合約))) | +| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((Storage[3] 又名我們代理的合約))) | +| 12C | SWAP1 | 0x80 0xFF...FF (((Storage[3] 又名我們代理的合約))) | +| 12D | SWAP2 | (((Storage[3] 又名我們代理的合約))) 0xFF...FF 0x80 | +| 12E | AND | ProxyAddr 0x80 | +| 12F | DUP2 | 0x80 ProxyAddr 0x80 | +| 130 | MSTORE | 0x80 | + +而 0x80 現在包含代理地址 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | --------- | +| 131 | PUSH1 0x20 | 0x20 0x80 | +| 133 | ADD | 0xA0 | +| 134 | PUSH2 0x00e4 | 0xE4 0xA0 | +| 137 | JUMP | 0xA0 | + +### E4 程式碼 {#the-e4-code} + +這是我們第一次看到這些行,但它們與其他方法共用(見下文)。 所以我們將堆疊中的值稱為 X,並記住,在 `splitter()` 中,這個 X 的值是 0xA0。 + +| 位移 | Opcode | 堆疊 | +| -: | ---------- | ----------- | +| E4 | JUMPDEST | X | +| E5 | PUSH1 0x40 | 0x40 X | +| E7 | MLOAD | 0x80 X | +| E8 | DUP1 | 0x80 0x80 X | +| E9 | SWAP2 | X 0x80 0x80 | +| EA | SUB | X-0x80 0x80 | +| EB | SWAP1 | 0x80 X-0x80 | +| EC | RETURN | | + +所以這段程式碼在堆疊中接收一個記憶體指標 (X),並使合約以一個 0x80 - X 的緩衝區 `RETURN`。 + +在 `splitter()` 的情況下,這會返回我們代理的地址。 `RETURN` 返回 0x80-0x9F 中的緩衝區,這是我們寫入此資料的位置(上面的位移 0x130)。 + +## currentWindow() {#currentwindow} + +位移 0x158-0x163 中的程式碼與我們在 `splitter()` 的 0x103-0x10E 中看到的相同(除了 `JUMPI` 目的地),所以我們知道 `currentWindow()` 也不是 `payable`。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ------------------------------------------------------------------------ | +| 164 | JUMPDEST | | +| 165 | POP | | +| 166 | PUSH2 0x00da | 0xDA | +| 169 | PUSH1 0x01 | 0x01 0xDA | +| 16B | SLOAD | Storage[1] 0xDA | +| 16C | DUP2 | 0xDA Storage[1] 0xDA | +| 16D | JUMP | Storage[1] 0xDA | + +### DA 程式碼 {#the-da-code} + +這段程式碼也與其他方法共用。 所以我們將堆疊中的值稱為 Y,並記住,在 `currentWindow()` 中,這個 Y 的值是 Storage[1]。 + +| 位移 | Opcode | 堆疊 | +| -: | ---------- | ---------------- | +| DA | JUMPDEST | Y 0xDA | +| DB | PUSH1 0x40 | 0x40 Y 0xDA | +| DD | MLOAD | 0x80 Y 0xDA | +| DE | SWAP1 | Y 0x80 0xDA | +| DF | DUP2 | 0x80 Y 0x80 0xDA | +| E0 | MSTORE | 0x80 0xDA | + +將 Y 寫入 0x80-0x9F。 + +| 位移 | Opcode | 堆疊 | +| -: | ---------- | -------------- | +| E1 | PUSH1 0x20 | 0x20 0x80 0xDA | +| E3 | ADD | 0xA0 0xDA | + +其餘的已在[上面](#the-e4-code)解釋過了。 所以跳轉到 0xDA 會將堆疊頂部 (Y) 寫入 0x80-0x9F,並返回該值。 在 `currentWindow()` 的情況下,它返回 Storage[1]。 + +## merkleRoot() {#merkleroot} + +位移 0xED-0xF8 中的程式碼與我們在 `splitter()` 的 0x103-0x10E 中看到的相同(除了 `JUMPI` 目的地),所以我們知道 `merkleRoot()` 也不是 `payable`。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ------------------------------------------------------------------------ | +| F9 | JUMPDEST | | +| FA | POP | | +| FB | PUSH2 0x00da | 0xDA | +| FE | PUSH1 0x00 | 0x00 0xDA | +| 100 | SLOAD | Storage[0] 0xDA | +| 101 | DUP2 | 0xDA Storage[0] 0xDA | +| 102 | JUMP | Storage[0] 0xDA | + +跳轉後發生的事情[我們已經弄清楚了](#the-da-code)。 所以 `merkleRoot()` 返回 Storage[0]。 + +## 0x81e580d3 {#0x81e580d3} + +位移 0x138-0x143 中的程式碼與我們在 `splitter()` 的 0x103-0x10E 中看到的相同(除了 `JUMPI` 目的地),所以我們知道這個函數也不是 `payable`。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ------------------------------------------------------------------------------- | +| 144 | JUMPDEST | | +| 145 | POP | | +| 146 | PUSH2 0x00da | 0xDA | +| 149 | PUSH2 0x0153 | 0x0153 0xDA | +| 14C | CALLDATASIZE | CALLDATASIZE 0x0153 0xDA | +| 14D | PUSH1 0x04 | 0x04 CALLDATASIZE 0x0153 0xDA | +| 14F | PUSH2 0x018f | 0x018F 0x04 CALLDATASIZE 0x0153 0xDA | +| 152 | JUMP | 0x04 CALLDATASIZE 0x0153 0xDA | +| 18F | JUMPDEST | 0x04 CALLDATASIZE 0x0153 0xDA | +| 190 | PUSH1 0x00 | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 192 | PUSH1 0x20 | 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 194 | DUP3 | 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 195 | DUP5 | CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 196 | SUB | CALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 197 | SLT | CALLDATASIZE-4\<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 198 | ISZERO | CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 199 | PUSH2 0x01a0 | 0x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19C | JUMPI | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | + +看起來這個函數至少需要 32 個位元組(一個字)的呼叫資料。 + +| 位移 | Opcode | 堆疊 | +| --: | ------ | -------------------------------------------- | +| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19F | REVERT | | + +如果它沒有得到呼叫資料,交易會被還原而沒有任何返回資料。 + +讓我們看看如果函數_確實_得到了它需要的呼叫資料會發生什麼。 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | ----------------------------------------------------------- | +| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA | +| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA | + +`calldataload(4)` 是呼叫資料中方法簽名_之後_的第一個字 + +| 位移 | Opcode | 堆疊 | +| --: | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1A3 | SWAP2 | 0x0153 CALLDATASIZE calldataload(4) 0xDA | +| 1A4 | SWAP1 | CALLDATASIZE 0x0153 calldataload(4) 0xDA | +| 1A5 | POP | 0x0153 calldataload(4) 0xDA | +| 1A6 | JUMP | calldataload(4) 0xDA | +| 153 | JUMPDEST | calldataload(4) 0xDA | +| 154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA | +| 157 | JUMP | calldataload(4) 0xDA | +| 16E | JUMPDEST | calldataload(4) 0xDA | +| 16F | PUSH1 0x04 | 0x04 calldataload(4) 0xDA | +| 171 | DUP2 | calldataload(4) 0x04 calldataload(4) 0xDA | +| 172 | DUP2 | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA | +| 173 | SLOAD | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA | +| 174 | DUP2 | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA | +| 175 | LT | calldataload(4)\)`,另一個是 `isClaimed()`,所以它看起來像一個空投合約。 與其逐個 opcode 瀏覽其餘部分,我們可以[嘗試反編譯器](https://etherscan.io/bytecode-decompiler?a=0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761),它從這個合約中為三個函數產生了可用的結果。 對其他函數的逆向工程留給讀者作為練習。 + +### scaleAmountByPercentage {#scaleamountbypercentage} + +以下是反編譯器為此函數提供的內容: + +```python +def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable: + require calldata.size - 4 >=′ 64 + if _param1 and _param2 > -1 / _param1: + revert with 0, 17 + return (_param1 * _param2 / 100 * 10^6) +``` + +第一個 `require` 測試呼叫資料除了函數簽名的四個位元組外,至少還有 64 個位元組,足以容納兩個參數。 如果不是,顯然有問題。 + +`if` 語句似乎檢查 `_param1` 是否不為零,以及 `_param1 * _param2` 是否不為負。 這可能是為了防止環繞的情況。 + +最後,函數返回一個縮放後的值。 + +### 申領 {#claim} + +反編譯器創建的程式碼很複雜,並非所有程式碼都與我們相關。 我將跳過其中一部分,專注於我認為提供有用資訊的行 + +```python +def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable: + ... + require _param2 == addr(_param2) + ... + if currentWindow <= _param1: + revert with 0, 'cannot claim for a future window' +``` + +我們在這裡看到兩個重要的事情: + +- `_param2`,雖然它被聲明為 `uint256`,但實際上是一個地址 +- `_param1` 是被申領的窗口,必須是 `currentWindow` 或更早。 + +```python + ... + if stor5[_claimWindow][addr(_claimFor)]: + revert with 0, 'Account already claimed the given window' +``` + +所以現在我們知道 Storage[5] 是一個視窗和地址的陣列,以及地址是否已為該視窗申領獎勵。 + +```python + ... + idx = 0 + s = 0 + while idx < _param4.length: + ... + if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]: + mem[mem[64] + 32] = mem[(32 * idx) + 296] + ... + s = sha3(mem[_62 + 32 len mem[_62]]) + continue + ... + s = sha3(mem[_66 + 32 len mem[_66]]) + continue + if unknown2eb4a7ab != s: + revert with 0, 'Invalid proof' +``` + +我們知道 `unknown2eb4a7ab` 實際上是 `merkleRoot()` 函數,所以這段程式碼看起來像是在驗證一個 [merkle 證明](https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5)。 這意味著 `_param4` 是一個 merkle 證明。 + +```python +call addr(_param2) with: + value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei + gas 30000 wei +``` + +這是一個合約將自己的 ETH 轉移到另一個地址(合約或外部擁有)的方式。 它以要轉移的金額作為值來呼叫它。 所以看起來這是一次 ETH 空投。 + +```python +if not return_data.size: + if not ext_call.success: + require ext_code.size(stor2) + call stor2.deposit() with: + value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei +``` + +最下面兩行告訴我們,Storage[2] 也是我們呼叫的一個合約。 如果我們[查看建構子交易](https://etherscan.io/tx/0xa1ea0549fb349eb7d3aff90e1d6ce7469fdfdcd59a2fd9b8d1f5e420c0d05b58#statechange),我們會看到這個合約是 [0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2),一個 [其原始碼已上傳到 Etherscan 的](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code) 包裝以太幣合約。 + +所以看起來合約試圖將 ETH 發送給 `_param2`。 如果能成功,那就太好了。 如果不行,它會嘗試發送 [WETH](https://weth.tkn.eth.limo/)。 如果 `_param2` 是一個外部擁有的帳戶 (EOA),那麼它總是可以接收 ETH,但合約可以拒絕接收 ETH。 然而,WETH 是 ERC-20,合約不能拒絕接受它。 + +```python +log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success) +``` + +在函數的末尾,我們看到一個日誌條目正在生成。 [查看生成的日誌條目](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#events) 並過濾以 `0xdbd5...` 開頭的主題。 如果我們[點擊其中一筆產生此類條目的交易](https://etherscan.io/tx/0xe7d3b7e00f645af17dfbbd010478ef4af235896c65b6548def1fe95b3b7d2274),我們會看到它確實看起來像一次申領——該帳戶向我們正在進行逆向工程的合約發送了一條訊息,並作為回報收到了 ETH。 + +![一次申領交易](claim-tx.png) + +### 1e7df9d3 {#1e7df9d3} + +這個函數與上面的 [`claim`](#claim) 非常相似。 它還會檢查 merkle 證明,嘗試將 ETH 轉移給第一個,並產生相同類型的日誌條目。 + +```python +def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable: + ... + idx = 0 + s = 0 + while idx < _param3.length: + if idx >= mem[96]: + revert with 0, 50 + _55 = mem[(32 * idx) + 128] + if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]: + ... + s = sha3(mem[_58 + 32 len mem[_58]]) + continue + mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) + ... + if unknown2eb4a7ab != s: + revert with 0, 'Invalid proof' + ... + call addr(_param1) with: + value s wei + gas 30000 wei + if not return_data.size: + if not ext_call.success: + require ext_code.size(stor2) + call stor2.deposit() with: + value s wei + gas gas_remaining wei + ... + log 0xdbd5389f: addr(_param1), s, bool(ext_call.success) +``` + +主要區別在於第一個參數,即要提領的視窗,不存在。 取而代之的是,有一個迴圈遍歷所有可以申領的視窗。 + +```python + idx = 0 + s = 0 + while idx < currentWindow: + ... + if stor5[mem[0]]: + if idx == -1: + revert with 0, 17 + idx = idx + 1 + s = s + continue + ... + stor5[idx][addr(_param1)] = 1 + if idx >= unknown81e580d3.length: + revert with 0, 50 + mem[0] = 4 + if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]: + revert with 0, 17 + if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6): + revert with 0, 17 + if idx == -1: + revert with 0, 17 + idx = idx + 1 + s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6) + continue +``` + +所以它看起來像一個申領所有視窗的 `claim` 變體。 + +## 結論 {#conclusion} + +到目前為止,您應該知道如何使用 opcodes 或(當它起作用時)反編譯器來理解沒有原始碼的合約。 從本文的篇幅可以明顯看出,對合約進行逆向工程並非易事,但在一個安全至關重要的系統中,能夠驗證合約是否如承諾般運作是一項重要的技能。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/run-node-raspberry-pi/index.md b/public/content/translations/zh-tw/developers/tutorials/run-node-raspberry-pi/index.md new file mode 100644 index 00000000000..7ecbdbb2b64 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/run-node-raspberry-pi/index.md @@ -0,0 +1,184 @@ +--- +title: "在 Raspberry Pi 4 上執行一個以太坊節點" +description: "為您的 Raspberry Pi 4 刷機、插入乙太網路線、連接 SSD 硬碟並開啟裝置電源,即可將 Raspberry Pi 4 變成一個完整的以太坊節點 + 驗證程式" +author: "EthereumOnArm" +tags: [ "clients", "execution layer", "consensus layer", "nodes" ] +lang: zh-tw +skill: intermediate +published: 2022-06-10 +source: Ethereum on ARM +sourceUrl: https://ethereum-on-arm-documentation.readthedocs.io/en/latest/ +--- + +**Ethereum on Arm 是一個自訂的 Linux 映像檔,可以將 Raspberry Pi 變成一個以太坊節點。** + +要使用 Ethereum on Arm 將 Raspberry Pi 變成一個以太坊節點,建議使用以下硬體: + +- Raspberry 4 (B 型 8GB)、Odroid M1 或 Rock 5B (8GB/16GB RAM) 開發板 +- MicroSD 卡 (最低 16 GB Class 10) +- 最低 2 TB SSD USB 3.0 磁碟,或附帶 USB 轉 SATA 外接盒的 SSD。 +- 電源供應器 +- 乙太網路線 +- 通訊埠轉發 (詳情請參閱用戶端) +- 附散熱片和風扇的機殼 +- USB 鍵盤、螢幕和 HDMI 連接線 (micro-HDMI) (選用) + +## 為什麼要在 ARM 上執行以太坊? {#why-run-ethereum-on-arm} + +ARM 開發板是非常實惠、靈活的小型電腦。 它們是執行以太坊節點的絕佳選擇,因為它們價格低廉,可以設定成將所有資源都集中在節點上,從而提高效率,而且它們耗電量低、體積小,可以不佔空間地放置在家中任何地方。 啟動節點也非常容易,因為 Raspberry Pi 的 MicroSD 只要用預先建置的映像檔刷機即可,不需要下載或建置軟體。 + +## 它是如何運作的? {#how-does-it-work} + +Raspberry Pi 的記憶卡已用預先建置的映像檔刷機。 此映像檔包含執行以太坊節點所需的一切。 使用已刷機的記憶卡,使用者只需要開啟 Raspberry Pi 的電源。 所有執行節點所需的程序都會自動啟動。 這是可行的,因為記憶卡包含一個以 Linux 為基礎的作業系統 (OS),系統級的程序會在其上自動執行,將裝置變成一個以太坊節點。 + +無法使用熱門的 Raspberry Pi Linux 作業系統「Raspbian」來執行以太坊,因為 Raspbian 仍使用 32 位元架構,這會導致以太坊使用者遇到記憶體問題,而且共識用戶端不支援 32 位元二進位檔。 為了克服這個問題,Ethereum on Arm 團隊遷移到一個名為「Armbian」的原生 64 位元作業系統。 + +**映像檔會處理所有必要的步驟**,從設定環境和格式化 SSD 磁碟,到安裝並執行以太坊軟體,以及開始區塊鏈同步。 + +## 關於執行與共識用戶端的注意事項 {#note-on-execution-and-consensus-clients} + +Ethereum on Arm 映像檔包含預先建置的執行用戶端和共識用戶端作為服務。 以太坊節點需要兩個用戶端都保持同步和執行。 您只需要下載並刷入映像檔,然後啟動服務。 映像檔預先載入了以下執行用戶端: + +- Geth +- Nethermind +- Besu + +以及下列共識用戶端: + +- Lighthouse +- Nimbus +- Prysm +- Teku + +您應該各選擇一個來執行,所有執行用戶端都與所有共識用戶端相容。 如果您沒有明確選擇用戶端,節點將會使用預設值 Geth 和 Lighthouse,並在開發板通電時自動執行它們。 您必須在路由器上開啟通訊埠 30303,這樣 Geth 才能找到並連接到對等點。 + +## 下載映像檔 {#downloading-the-image} + +Raspberry Pi 4 以太坊映像檔是一個「隨插即用」的映像檔,它會自動安裝和設定執行用戶端和共識用戶端,並設定它們彼此通訊及連接到以太坊網路。 使用者只需要使用一個簡單的指令來啟動他們的程序。 + +從 [Ethereum on Arm](https://ethereumonarm-my.sharepoint.com/:u:/p/dlosada/Ec_VmUvr80VFjf3RYSU-NzkBmj2JOteDECj8Bibde929Gw?download=1) 下載 Raspberry Pi 映像檔並驗證 SHA256 雜湊值: + +```sh +# 從包含已下載映像檔的目錄 +shasum -a 256 ethonarm_22.04.00.img.zip +# 雜湊值應輸出: fb497e8f8a7388b62d6e1efbc406b9558bee7ef46ec7e53083630029c117444f +``` + +請注意,Rock 5B 和 Odroid M1 開發板的映像檔可在 Ethereum-on-Arm 的 [下載頁面](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/quick-guide/download-and-install.html) 取得。 + +## 刷入 MicroSD {#flashing-the-microsd} + +要用於 Raspberry Pi 的 MicroSD 卡應先插入桌上型電腦或筆記型電腦以進行刷機。 然後,以下終端機指令會將下載的映像檔刷入 SD 卡: + +```shell +# 檢查 MicroSD 卡名稱 +sudo fdisk -l + +>> sdxxx +``` + +正確取得名稱非常重要,因為下一個指令包含 `dd`,它會在將映像檔推送到記憶卡之前完全清除其現有內容。 若要繼續,請導覽至包含壓縮映像檔的目錄: + +```shell +# 解壓縮並刷入映像檔 +unzip ethonarm_22.04.00.img.zip +sudo dd bs=1M if=ethonarm_22.04.00.img of=/dev/ conv=fdatasync status=progress +``` + +記憶卡現在已刷機完成,可以插入 Raspberry Pi 了。 + +## 啟動節點 {#start-the-node} + +將 SD 卡插入 Raspberry Pi 後,連接乙太網路線和 SSD,然後開啟電源。 作業系統將會啟動,並自動開始執行預先設定的任務,將 Raspberry Pi 變成一個以太坊節點,包括安裝和建置用戶端軟體。 這可能需要 10-15 分鐘。 + +一旦所有內容都安裝並設定完成,請透過 ssh 連線登入裝置,或者如果開發板上連接了螢幕和鍵盤,則直接使用終端機登入。 使用 `ethereum` 帳戶登入,因為它具有啟動節點所需的權限。 + +```shell +使用者:ethereum +密碼:ethereum +``` + +預設的執行用戶端 Geth 將會自動啟動。 您可以透過使用以下終端機指令檢查記錄來確認這一點: + +```sh +sudo journalctl -u geth -f +``` + +共識用戶端確實需要明確地啟動。 為此,請先在路由器上開啟通訊埠 9000,以便 Lighthouse 可以找到並連接到對等點。 然後啟用並啟動 lighthouse 服務: + +```sh +sudo systemctl enable lighthouse-beacon +sudo systemctl start lighthouse-beacon +``` + +使用記錄檢查用戶端: + +```sh +sudo journalctl -u lighthouse-beacon +``` + +請注意,共識用戶端將在幾分鐘內同步,因為它使用檢查點同步。 執行用戶端將需要更長的時間,可能需要幾個小時,並且在共識用戶端同步完成之前不會啟動 (這是因為執行用戶端需要一個同步目標,而同步完成的共識用戶端會提供該目標)。 + +在 Geth 和 Lighthouse 服務執行並同步後,您的 Raspberry Pi 現在就是一個以太坊節點了! 最常見的與以太坊網路互動的方式是使用 Geth 的 Javascript 主控台,它可以附加到通訊埠 8545 上的 Geth 用戶端。 也可以使用像 Curl 這樣的請求工具來提交格式化為 JSON 物件的指令。 更多資訊請參閱 [Geth 文件](https://geth.ethereum.org/)。 + +Geth 已預先設定為向 Grafana 儀表板回報指標,該儀表板可在瀏覽器中檢視。 更進階的使用者可能會希望使用此功能來監控其節點的健康狀況,方法是導覽至 `ipaddress:3000`,並傳入 `user: admin` 和 `passwd: ethereum`。 + +## 驗證程式 {#validators} + +也可以選擇性地將驗證程式新增至共識用戶端。 驗證程式軟體可讓您的節點積極參與共識,並為網路提供加密經濟安全性。 您將會因這項工作獲得 ETH 作為酬勞。 若要執行驗證程式,您必須先擁有 32 枚 ETH,這些 ETH 必須存入存款合約中。 可以按照 [Launchpad](https://launchpad.ethereum.org/) 上的逐步指南進行存款。 請在桌上型電腦/筆記型電腦上執行此操作,但不要產生金鑰 — 這可以直接在 Raspberry Pi 上完成。 + +在 Raspberry Pi 上開啟一個終端機,並執行以下指令來產生存款金鑰: + +``` +sudo apt-get update +sudo apt-get install staking-deposit-cli +cd && deposit new-mnemonic --num_validators 1 +``` + +(或下載 [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) 在離線機器上執行,並執行 `deposit new-mnemnonic` 指令) + +請妥善保管您的助憶詞! 上述指令在節點的密鑰存儲檔案中產生了兩個檔案:驗證程式金鑰和一個存款資料檔案。 存款資料需要上傳到 Launchpad,因此必須將其從 Raspberry Pi 複製到桌上型電腦/筆記型電腦。 這可以透過 ssh 連線或任何其他複製/貼上方法來完成。 + +一旦執行 Launchpad 的電腦上有了存款資料檔案,就可以將其拖放到 Launchpad 畫面上的 `+` 號上。 按照螢幕上的指示將一筆交易傳送到存款合約。 + +回到 Raspberry Pi 上,就可以啟動一個驗證程式了。 這需要匯入驗證程式金鑰、設定收取酬勞的地址,然後啟動預先設定的驗證程式程序。 以下範例適用於 Lighthouse—其他共識用戶端的說明可在 [Ethereum on Arm 文件](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/) 中找到: + +```shell +# 匯入驗證程式金鑰 +lighthouse account validator import --directory=/home/ethereum/validator_keys + +# 設定酬勞地址 +sudo sed -i 's/' /etc/ethereum/lighthouse-validator.conf + +# 啟動驗證程式 +sudo systemctl start lighthouse-validator +``` + +恭喜,您現在已經在 Raspberry Pi 上執行一個完整的以太坊節點和驗證程式了! + +## 更多詳細資訊 {#more-details} + +本頁面概述了如何使用 Raspberry Pi 設定 Geth-Lighthouse 節點和驗證程式。 更詳細的說明可在 [Ethereum-on-Arm 網站](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/index.html) 上取得。 + +## 感謝您的回饋 {#feedback-appreciated} + +我們知道 Raspberry Pi 擁有龐大的使用者群,這對以太坊網路的健康狀況可能產生非常正面的影響。 +請深入研究本使用教學中的詳細資訊、嘗試在測試網上執行、查看 Ethereum on Arm 的 GitHub、提供回饋、提出問題和提取請求,並協助推動技術和文件的進步! + +## 參考資料 {#references} + +1. https://ubuntu.com/download/raspberry-pi +2. https://wikipedia.org/wiki/Port_forwarding +3. https://prometheus.io +4. https://grafana.com +5. https://forum.armbian.com/topic/5565-zram-vs-swap/ +6. https://geth.ethereum.org +7. https://nethermind.io +8. https://www.hyperledger.org/projects/besu +9. https://github.com/prysmaticlabs/prysm +10. https://lighthouse.sigmaprime.io +11. https://ethersphere.github.io/swarm-home +12. https://raiden.network +13. https://ipfs.io +14. https://status.im +15. https://vipnode.org diff --git a/public/content/translations/zh-tw/developers/tutorials/scam-token-tricks/index.md b/public/content/translations/zh-tw/developers/tutorials/scam-token-tricks/index.md new file mode 100644 index 00000000000..1bec13fe447 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/scam-token-tricks/index.md @@ -0,0 +1,470 @@ +--- +title: "詐騙代幣使用的一些伎倆以及如何偵測它們" +description: "在本使用教學中,我們將剖析一個詐騙代幣,以了解詐騙者所玩的伎倆、他們如何實作這些伎倆,以及我們該如何偵測它們。" +author: Ori Pomerantz +tags: + [ + "scam", + "solidity", + "erc-20", + "javascript", + "typescript" + ] +skill: intermediate +published: 2023-09-15 +lang: zh-tw +--- + +在本使用教學中,我們將剖析[一個詐騙代幣](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code),以了解詐騙者所玩的伎倆以及他們如何實作這些伎倆。 在本使用教學結束時,您將對 ERC-20 代幣合約、其功能以及為何保持懷疑是必要的有更全面的了解。 然後我們會查看該詐騙代幣發出的事件,並了解如何自動識別它不是合法的。 + +## 詐騙代幣 - 它們是什麼、為什麼人們會做,以及如何避免它們 {#scam-tokens} + +以太坊最常見的用處之一就是為一個團隊建立一種可交易的代幣。某種意義上,這是屬於他們自己的貨幣。 然而,任何能產生價值的正當使用案例中,就會有犯罪者嘗試竊取該價值納為已用。 + +您可以從使用者角度在 [ethereum.org 的其他地方](/guides/how-to-id-scam-tokens/)閱讀更多關於此主題的資訊。 本使用教學著重於剖析詐騙代幣,以了解其運作方式以及如何偵測。 + +### 我如何知道 wARB 是個詐騙? {#warb-scam} + +我們剖析的代幣是 [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code),它偽裝成與合法的 [ARB 代幣](https://etherscan.io/token/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1)等效。 + +要知道哪個是合法代幣,最簡單的方法是查看其發起組織 [Arbitrum](https://arbitrum.foundation/)。 合法的地址已在其[文件中](https://docs.arbitrum.foundation/deployment-addresses#token)指明。 + +### 為什麼原始碼是可用的? {#why-source} + +通常,我們期望試圖詐騙他人的人會行事隱密,而事實上許多詐騙代幣也沒有提供其程式碼 (例如,[這一個](https://optimistic.etherscan.io/token/0x15992f382d8c46d667b10dc8456dc36651af1452#code) 和 [這一個](https://optimistic.etherscan.io/token/0x026b623eb4aada7de37ef25256854f9235207178#code))。 + +然而,合法的代幣通常會公布其原始碼,因此為了看起來合法,詐騙代幣的作者有時也會這麼做。 [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code) 是那些提供原始碼的代幣之一,這讓我們更容易理解它。 + +雖然合約部署者可以選擇是否公布原始碼,但他們_無法_公布錯誤的原始碼。 區塊瀏覽器會獨立編譯提供的原始碼,如果沒有得到完全相同的位元組碼,它就會拒絕該原始碼。 [您可以在 Etherscan 網站上閱讀更多相關資訊](https://etherscan.io/verifyContract)。 + +## 與合法的 ERC-20 代幣比較 {#compare-legit-erc20} + +我們將把這個代幣與合法的 ERC-20 代幣進行比較。 如果您不熟悉合法的 ERC-20 代幣通常是如何編寫的,請[參見本使用教學](/developers/tutorials/erc20-annotated-code/)。 + +### 特權地址的常數 {#constants-for-privileged-addresses} + +合約有時需要特權地址。 設計用於長期使用的合約允許某些特權地址更改這些地址,例如啟用新的多重簽名合約。 有幾種方法可以做到這一點。 + +[`HOP` 代幣合約](https://etherscan.io/address/0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc#code) 使用 [`Ownable`](https://docs.openzeppelin.com/contracts/2.x/access-control#ownership-and-ownable) 模式。 特權地址保存在儲存空間中,位於一個名為 `_owner` 的欄位(請參見第三個檔案 `Ownable.sol`)。 + +```solidity +abstract contract Ownable is Context { + address private _owner; + . + . + . +} +``` + +[`ARB` 代幣合約](https://etherscan.io/address/0xad0c361ef902a7d9851ca7dcc85535da2d3c6fc7#code) 沒有直接的特權地址。 然而,它並不需要。 它位於[地址 `0xb50721bcf8d664c30412cfbc6cf7a15145234ad1`](https://etherscan.io/address/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1#code) 的一個[`代理`](https://docs.openzeppelin.com/contracts/5.x/api/proxy) 後方。 該合約有一個可用於升級的特權地址 (請參見第四個檔案,`ERC1967Upgrade.sol`)。 + +```solidity + /** + * @dev 在 EIP1967 管理員時隙中儲存一個新地址。 + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } +``` + +相比之下,`wARB` 合約有一個硬編碼的 `contract_owner`。 + +```solidity +contract WrappedArbitrum is Context, IERC20 { + . + . + . + address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1; + address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33; + . + . + . +} +``` + +[這個合約擁有者](https://etherscan.io/address/0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33) 不是一個可以在不同時間由不同帳戶控制的合約,而是一個[外部擁有的帳戶](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs)。 這意味著它可能是為個人短期使用而設計的,而不是作為控制一個將保持價值的 ERC-20 的長期解決方案。 + +事實上,如果我們在 Etherscan 上查看,會發現詐騙者在 2023 年 5 月 19 日期間只使用了這個合約 12 個小時(從[第一筆交易](https://etherscan.io/tx/0xf49136198c3f925fcb401870a669d43cecb537bde36eb8b41df77f06d5f6fbc2)到[最後一筆交易](https://etherscan.io/tx/0xdfd6e717157354e64bbd5d6adf16761e5a5b3f914b1948d3545d39633244d47b))。 + +### 虛假的 `_transfer` 函數 {#the-fake-transfer-function} + +標準做法是使用[一個內部 `_transfer` 函數](/developers/tutorials/erc20-annotated-code/#the-_transfer-function-_transfer) 來進行實際的傳送。 + +在 `wARB` 中,這個函數看起來幾乎是合法的: + +```solidity + function _transfer(address sender, address recipient, uint256 amount) internal virtual{ + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + if (sender == contract_owner){ + sender = deployer; + } + emit Transfer(sender, recipient, amount); + } +``` + +可疑的部分是: + +```solidity + if (sender == contract_owner){ + sender = deployer; + } + emit Transfer(sender, recipient, amount); +``` + +如果合約擁有者傳送代幣,為什麼 `Transfer` 事件顯示它們來自 `deployer`? + +然而,還有一個更重要的問題。 誰調用了這個 `_transfer` 函數? 它不能從外部調用,因為它被標記為 `internal`。 而且我們擁有的程式碼不包含任何對 `_transfer` 的調用。 顯然,它在這裡是作為一個誘餌。 + +```solidity + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _f_(_msgSender(), recipient, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _f_(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } +``` + +當我們查看被調用來傳送代幣的函數,`transfer` 和 `transferFrom` 時,我們發現它們調用了一個完全不同的函數,`_f_`。 + +### 真正的 `_f_` 函數 {#the-real-f-function} + +```solidity + function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + if (sender == contract_owner){ + + sender = deployer; + } + emit Transfer(sender, recipient, amount); + } +``` + +這個函數中有兩個潛在的危險信號。 + +- 使用 [函數修飾符](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm) `_mod_`。 然而,當我們查看原始碼時,我們發現 `_mod_` 實際上是無害的。 + + ```solidity + modifier _mod_(address sender, address recipient, uint256 amount){ + _; + } + ``` + +- 我們在 `_transfer` 中看到的同樣問題,就是當 `contract_owner` 傳送代幣時,它們看起來像是來自 `deployer`。 + +### 虛假事件函數 `dropNewTokens` {#the-fake-events-function-dropNewTokens} + +現在我們來看一個看起來像實際詐騙的東西。 為了可讀性,我對函數做了一點編輯,但它在功能上是等效的。 + +```solidity +function dropNewTokens(address uPool, + address[] memory eReceiver, + uint256[] memory eAmounts) public auth() +``` + +這個函數有 `auth()` 修飾符,這意味著它只能被合約擁有者調用。 + +```solidity +modifier auth() { + require(msg.sender == contract_owner, "Not allowed to interact"); + _; +} +``` + +這個限制完全合理,因為我們不希望隨機帳戶分發代幣。 然而,函數的其餘部分是可疑的。 + +```solidity +{ + for (uint256 i = 0; i < eReceiver.length; i++) { + emit Transfer(uPool, eReceiver[i], eAmounts[i]); + } +} +``` + +一個將資金從一個池帳戶轉移到一個接收者陣列並對應一個金額陣列的函數是完全合理的。 在許多使用案例中,您會希望將代幣從單一來源分發到多個目的地,例如薪資發放、空投等。 在單一交易中完成此操作比發出多個交易,或甚至在同一交易中從不同的合約多次調用 ERC-20 更便宜 (在 gas 方面)。 + +然而,`dropNewTokens` 並沒有這樣做。 它發出 [`Transfer` 事件](https://eips.ethereum.org/EIPS/eip-20#transfer-1),但實際上並沒有傳送任何代幣。 沒有任何正當理由去告訴鏈外應用程式一筆並未真正發生的傳送,從而混淆它們。 + +### 銷毀的 `Approve` 函數 {#the-burning-approve-function} + +ERC-20 合約應該有一個用於授權的 [`approve` 函數](/developers/tutorials/erc20-annotated-code/#approve),而我們的詐騙代幣確實有這樣一個函數,而且它甚至是正確的。 然而,因為 Solidity 源自 C 語言,所以它區分大小寫。 "Approve" 和 "approve" 是不同的字串。 + +此外,其功能與 `approve` 無關。 + +```solidity + function Approve( + address[] memory holders) +``` + +這個函數被調用時,會傳入一個代幣持有者的地址陣列。 + +```solidity + public approver() { +``` + +`approver()` 修飾符確保只有 `contract_owner` 能夠調用此函數(見下文)。 + +```solidity + for (uint256 i = 0; i < holders.length; i++) { + uint256 amount = _balances[holders[i]]; + _beforeTokenTransfer(holders[i], 0x0000000000000000000000000000000000000001, amount); + _balances[holders[i]] = _balances[holders[i]].sub(amount, + "ERC20: burn amount exceeds balance"); + _balances[0x0000000000000000000000000000000000000001] = + _balances[0x0000000000000000000000000000000000000001].add(amount); + } + } + +``` + +對於每個持有者地址,該函數將持有者的全部餘額轉移到地址 `0x00...01`,實際上是銷毀了它(標準中的實際 `burn` 也會改變總供應量,並將代幣傳送到 `0x00...00`)。 這意味著 `contract_owner` 可以移除任何使用者的資產。 這似乎不是您希望在管理體系代幣中看到的功能。 + +### 程式碼品質問題 {#code-quality-issues} + +這些程式碼品質問題並不能_證明_這段程式碼是詐騙,但它們使其顯得可疑。 像 Arbitrum 這樣的有組織的公司通常不會發布這麼差的程式碼。 + +#### `mount` 函數 {#the-mount-function} + +雖然在[標準](https://eips.ethereum.org/EIPS/eip-20)中沒有指定,但一般來說,創建新代幣的函數稱為[`mint`](https://ethereum.org/el/developers/tutorials/erc20-annotated-code/#the-_mint-and-_burn-functions-_mint-and-_burn)。 + +如果我們查看 `wARB` 的建構函式,我們會發現鑄幣函數由於某些原因被重新命名為 `mount`,並且為了效率,它被調用了五次,每次使用初始供應量的五分之一,而不是一次性處理全部金額。 + +```solidity + constructor () public { + + _name = "Wrapped Arbitrum"; + _symbol = "wARB"; + _decimals = 18; + uint256 initialSupply = 1000000000000; + + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + } +``` + +`mount` 函數本身也很可疑。 + +```solidity + function mount(address account, uint256 amount) public { + require(msg.sender == contract_owner, "ERC20: mint to the zero address"); +``` + +查看 `require`,我們看到只有合約擁有者被允許鑄幣。 這是合法的。 但錯誤訊息應該是 _only owner is allowed to mint_ 或類似的內容。 相反,它卻是無關的 _ERC20: mint to the zero address_。 對於鑄幣到零地址的正確測試是 `require(account != address(0), "")`,但該合約從未費心去檢查。 + +```solidity + _totalSupply = _totalSupply.add(amount); + _balances[contract_owner] = _balances[contract_owner].add(amount); + emit Transfer(address(0), account, amount); + } +``` + +還有兩個與鑄幣直接相關的可疑事實: + +- 有一個 `account` 參數,推測這應該是接收鑄幣金額的帳戶。 但增加的餘額實際上是 `contract_owner` 的。 + +- 雖然增加的餘額屬於 `contract_owner`,但發出的事件卻顯示了一筆到 `account` 的傳送。 + +### 為何同時有 `auth` 和 `approver`? 為什麼要有個什麼都不做的 `mod`? {#why-both-autho-and-approver-why-the-mod-that-does-nothing} + +這個合約包含三個修飾符:`_mod_`、`auth` 和 `approver`。 + +```solidity + modifier _mod_(address sender, address recipient, uint256 amount){ + _; + } +``` + +`_mod_` 接受三個參數,但對它們沒有做任何事情。 為什麼要有它? + +```solidity + modifier auth() { + require(msg.sender == contract_owner, "Not allowed to interact"); + _; + } + + modifier approver() { + require(msg.sender == contract_owner, "Not allowed to interact"); + _; + } +``` + +`auth` 和 `approver` 更有意義,因為它們檢查合約是否由 `contract_owner` 調用。 我們預期某些特權操作,例如鑄幣,會被限制在該帳戶。 然而,擁有兩個做_完全相同事情_的獨立函數有什麼意義呢? + +## 我們可以自動偵測到什麼? {#what-can-we-detect-automatically} + +我們可以透過查看 Etherscan 發現 `wARB` 是一個詐騙代幣。 然而,那是一個中心化的解決方案。 理論上,Etherscan 可能被顛覆或駭入。 能夠獨立判斷一個代幣是否合法會更好。 + +我們可以透過查看 ERC-20 代幣發出的事件,來使用一些技巧識別它是否可疑(可能是詐騙或寫得很差)。 + +## 可疑的 `Approval` 事件 {#suspicious-approval-events} + +[`Approval` 事件](https://eips.ethereum.org/EIPS/eip-20#approval) 只應該在直接請求下發生(相較之下,[`Transfer` 事件](https://eips.ethereum.org/EIPS/eip-20#transfer-1) 可能因為授權而發生)。 有關此問題以及為何請求需要直接而非透過合約中介的詳細解釋,請[參見 Solidity 文件](https://docs.soliditylang.org/en/v0.8.20/security-considerations.html#tx-origin)。 + +這意味著,授權從[外部擁有的帳戶](/developers/docs/accounts/#types-of-account)支出的 `Approval` 事件,必須來自源於該帳戶且目的地為 ERC-20 合約的交易。 任何其他來自外部擁有帳戶的授權都是可疑的。 + +這裡有一個[識別這類事件的程式](https://github.com/qbzzt/20230915-scam-token-detection),它使用 [viem](https://viem.sh/) 和 [TypeScript](https://www.typescriptlang.org/docs/)(一種具有型別安全的 JavaScript 變體)。 若要執行它: + +1. 將 `.env.example` 複製為 `.env`。 +2. 編輯 `.env` 以提供以太坊主網節點的 URL。 +3. 執行 `pnpm install` 來安裝必要的套件。 +4. 執行 `pnpm susApproval` 來尋找可疑的授權。 + +以下是逐行解釋: + +```typescript +import { + Address, + TransactionReceipt, + createPublicClient, + http, + parseAbiItem, +} from "viem" +import { mainnet } from "viem/chains" +``` + +從 `viem` 導入型別定義、函數和鏈定義。 + +```typescript +import { config } from "dotenv" +config() +``` + +讀取 `.env` 以取得 URL。 + +```typescript +const client = createPublicClient({ + chain: mainnet, + transport: http(process.env.URL), +}) +``` + +建立一個 Viem 用戶端。 我們只需要從區塊鏈讀取資料,所以這個用戶端不需要私密金鑰。 + +```typescript +const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82" +const fromBlock = 16859812n +const toBlock = 16873372n +``` + +可疑的 ERC-20 合約地址,以及我們將在其中尋找事件的區塊範圍。 節點提供商通常會限制我們讀取事件的能力,因為頻寬可能很昂貴。 幸運的是,`wARB` 在長達十八小時的時間內並未使用,所以我們可以尋找所有事件(總共只有 13 個)。 + +```typescript +const approvalEvents = await client.getLogs({ + address: testedAddress, + fromBlock, + toBlock, + event: parseAbiItem( + "event Approval(address indexed _owner, address indexed _spender, uint256 _value)" + ), +}) +``` + +這是向 Viem 請求事件資訊的方式。 當我們提供確切的事件簽章,包括欄位名稱時,它會為我們解析事件。 + +```typescript +const isContract = async (addr: Address): boolean => + await client.getBytecode({ address: addr }) +``` + +我們的演算法只適用於外部擁有的帳戶。 如果 `client.getBytecode` 返回任何位元組碼,這意味著它是一個合約,我們應該直接跳過它。 + +如果您以前沒用過 TypeScript,這個函數定義可能會看起來有點奇怪。 我們不只告訴它第一個(也是唯一一個)參數叫做 `addr`,還告訴它這個參數的型別是 `Address`。 同樣地,`: boolean` 部分告訴 TypeScript 這個函數的返回值是一個布林值。 + +```typescript +const getEventTxn = async (ev: Event): TransactionReceipt => + await client.getTransactionReceipt({ hash: ev.transactionHash }) +``` + +這個函數從事件中獲取交易收據。 我們需要收據來確保我們知道交易的目的地是什麼。 + +```typescript +const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => { +``` + +這是最重要的函數,它實際決定了一個事件是否可疑。 返回類型 `(Event | null)` 告訴 TypeScript 這個函數可以返回 `Event` 或 `null`。 如果事件不可疑,我們返回 `null`。 + +```typescript +const owner = ev.args._owner +``` + +Viem 有欄位名稱,所以它為我們解析了事件。 `_owner` 是將被花費的代幣的所有者。 + +```typescript +// 合約的授權不可疑 +if (await isContract(owner)) return null +``` + +如果擁有者是合約,則假設此授權不可疑。 要檢查合約的授權是否可疑,我們需要追蹤交易的完整執行過程,以查看它是否曾到達擁有者合約,以及該合約是否直接調用了 ERC-20 合約。 這比我們想做的要耗費更多資源。 + +```typescript +const txn = await getEventTxn(ev) +``` + +如果授權來自外部擁有的帳戶,則獲取引起它的交易。 + +```typescript +// 如果授權來自一個非交易 `from` 欄位的 EOA 擁有者,則該授權是可疑的 +if (owner.toLowerCase() != txn.from.toLowerCase()) return ev +``` + +我們不能只檢查字串是否相等,因為地址是十六進位的,所以它們包含字母。 有時候,例如在 `txn.from` 中,這些字母都是小寫的。 在其他情況下,例如 `ev.args._owner`,地址是[混合大小寫以用於錯誤識別](https://eips.ethereum.org/EIPS/eip-55)。 + +但如果交易不是來自擁有者,且該擁有者是外部擁有的,那麼我們就有了一筆可疑的交易。 + +```typescript +// 如果交易的目的地不是我們正在 +// 調查的 ERC-20 合約,那它也是可疑的 +if (txn.to.toLowerCase() != testedAddress) return ev +``` + +同樣地,如果交易的 `to` 地址,也就是第一個被調用的合約,不是正在調查的 ERC-20 合約,那麼它就是可疑的。 + +```typescript + // 如果沒有可疑的理由,返回 null。 + return null +} +``` + +如果兩個條件都不成立,那麼 `Approval` 事件就不可疑。 + +```typescript +const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev)) +const testResults = (await Promise.all(testPromises)).filter((x) => x != null) + +console.log(testResults) +``` + +[一個 `async` 函數](https://www.w3schools.com/js/js_async.asp) 會返回一個 `Promise` 物件。 使用常見的語法 `await x()`,我們等待該 `Promise` 完成後再繼續處理。 這在程式設計上很簡單且容易理解,但效率不高。 當我們等待特定事件的 `Promise` 完成時,我們已經可以開始處理下一個事件了。 + +這裡我們使用 [`map`](https://www.w3schools.com/jsref/jsref_map.asp) 來建立一個 `Promise` 物件的陣列。 然後我們使用 [`Promise.all`](https://www.javascripttutorial.net/es6/javascript-promise-all/) 來等待所有這些 promise 完成。 然後我們 [`filter`](https://www.w3schools.com/jsref/jsref_filter.asp) 那些結果以移除不可疑的事件。 + +### 可疑的 `Transfer` 事件 {#suspicious-transfer-events} + +識別詐騙代幣的另一種可能方法是查看它們是否有任何可疑的傳送。 例如,來自沒有那麼多代幣的帳戶的傳送。 您可以查看[如何實現此測試](https://github.com/qbzzt/20230915-scam-token-detection/blob/main/susTransfer.ts),但 `wARB` 沒有這個問題。 + +## 結論 {#conclusion} + +ERC-20 詐騙的自動偵測會受到[偽陰性](https://en.wikipedia.org/wiki/False_positives_and_false_negatives#False_negative_error)的影響,因為詐騙可以使用一個完全正常的 ERC-20 代幣合約,而該合約只是不代表任何真實的東西。 所以你應該總是嘗試_從可信賴的來源獲取代幣地址_。 + +自動偵測在某些情況下可以提供幫助,例如在 DeFi 領域,那裡有很多代幣需要自動處理。 但一如既往,[買者自負](https://www.investopedia.com/terms/c/caveatemptor.asp),請自行研究,並鼓勵您的使用者也這樣做。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/secret-state/index.md b/public/content/translations/zh-tw/developers/tutorials/secret-state/index.md new file mode 100644 index 00000000000..563c66b0e46 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/secret-state/index.md @@ -0,0 +1,733 @@ +--- +title: "使用零知識證明建立秘密狀態" +description: "鏈上遊戲有所限制,因為它們無法保存任何隱藏資訊。 閱讀本教學課程後,讀者將能夠結合零知識證明和伺服器元件,建立具有秘密狀態、鏈下元件的可驗證遊戲。 我們將透過建立踩地雷遊戲來示範此技術。" +author: Ori Pomerantz +tags: [ "server", "offchain", "centralized", "zero-knowledge", "zokrates", "mud" ] +skill: advanced +lang: zh-tw +published: 2025-03-15 +--- + +_在區塊鏈上沒有秘密_。 發佈在區塊鏈上的所有內容,每個人都可以公開讀取。 這是必要的,因為區塊鏈的基礎是任何人都能夠驗證它。 然而,遊戲通常仰賴秘密狀態。 例如,如果你能直接到區塊鏈瀏覽器上查看地圖,[踩地雷](https://en.wikipedia.org/wiki/Minesweeper_\(video_game\))遊戲就完全沒有意義了。 + +最簡單的解決方案是使用[伺服器元件](/developers/tutorials/server-components/)來保存秘密狀態。 然而,我們使用區塊鏈的原因是為了防止遊戲開發者作弊。 我們需要確保伺服器元件的誠實性。 伺服器可以提供狀態的哈希,並使用[零知識證明](/zero-knowledge-proofs/#why-zero-knowledge-proofs-are-important)來證明用於計算移動結果的狀態是正確的。 + +閱讀本文後,你將了解如何建立這類保存秘密狀態的伺服器、一個用於顯示狀態的用戶端,以及一個用於兩者之間通訊的鏈上元件。 我們將使用的主要工具有: + +| 工具 | 目的 | 已在版本上驗證 | +| --------------------------------------------- | -------------- | --------------------------------------: | +| [Zokrates](https://zokrates.github.io/) | 零知識證明及其驗證 | 1.1.9 | +| [Typescript](https://www.typescriptlang.org/) | 伺服器和用戶端的程式設計語言 | 5.4.2 | +| [Node](https://nodejs.org/en) | 執行伺服器 | 20.18.2 | +| [Viem](https://viem.sh/) | 與區塊鏈通訊 | 2.9.20 | +| [MUD](https://mud.dev/) | 鏈上資料管理 | 2.0.12 | +| [React](https://react.dev/) | 用戶端使用者介面 | 18.2.0 | +| [Vite](https://vitejs.dev/) | 提供用戶端程式碼 | 4.2.1 | + +## 踩地雷範例 {#minesweeper} + +[踩地雷](https://en.wikipedia.org/wiki/Minesweeper_\(video_game\)) 是一款包含秘密地雷區地圖的遊戲。 玩家選擇在特定位置挖掘。 如果該位置有地雷,遊戲就結束了。 否則,玩家會得到該位置周圍八個方格中的地雷數量。 + +此應用程式使用 [MUD](https://mud.dev/) 編寫,這是一個讓我們可以使用[鍵值資料庫](https://aws.amazon.com/nosql/key-value/)將資料儲存在鏈上,並自動將該資料與鏈下元件同步的框架。 除了同步之外,MUD 還能輕鬆提供存取控制,並讓其他使用者能[無需許可地擴充](https://mud.dev/guides/extending-a-world)我們的應用程式。 + +### 執行踩地雷範例 {#running-minesweeper-example} + +若要執行踩地雷範例: + +1. 確保您已[安裝先決條件](https://mud.dev/quickstart#prerequisites):[Node](https://mud.dev/quickstart#prerequisites)、[Foundry](https://book.getfoundry.sh/getting-started/installation)、[`git`](https://git-scm.com/downloads)、[`pnpm`](https://git-scm.com/downloads) 和 [`mprocs`](https://github.com/pvolok/mprocs)。 + +2. 複製儲存庫。 + + ```sh copy + git clone https://github.com/qbzzt/20240901-secret-state.git + ``` + +3. 安裝套件。 + + ```sh copy + cd 20240901-secret-state/ + pnpm install + npm install -g mprocs + ``` + + 如果 Foundry 是作為 `pnpm install` 的一部分安裝的,您需要重新啟動命令列 shell。 + +4. 編譯合約 + + ```sh copy + cd packages/contracts + forge build + cd ../.. + ``` + +5. 啟動程式(包括 [anvil](https://book.getfoundry.sh/anvil/) 區塊鏈)並等待。 + + ```sh copy + mprocs + ``` + + 請注意,啟動需要很長時間。 若要查看進度,請先使用向下箭頭捲動至 _contracts_ 索引標籤,以查看正在部署的 MUD 合約。 當您收到訊息 _Waiting for file changes…_ 時,表示合約已部署,後續進度將在 _server_ 索引標籤中顯示。 在那裡,您要等到收到訊息 _Verifier address: 0x...._。 + + 如果此步驟成功,您會看到 `mprocs` 螢幕,左側是不同的程序,右側是目前所選程序的主控台輸出。 + + ![mprocs 螢幕](./mprocs.png) + + 如果 `mprocs` 出現問題,您可以手動執行這四個程序,每個程序都在各自的命令列視窗中執行: + + - **Anvil** + + ```sh + cd packages/contracts + anvil --base-fee 0 --block-time 2 + ``` + + - **合約** + + ```sh + cd packages/contracts + pnpm mud dev-contracts --rpc http://127.0.0.1:8545 + ``` + + - **伺服器** + + ```sh + cd packages/server + pnpm start + ``` + + - **用戶端** + + ```sh + cd packages/client + pnpm run dev + ``` + +6. 現在您可以瀏覽至[用戶端](http://localhost:3000),點擊 **New Game**,然後開始遊戲。 + +### 資料表 {#tables} + +我們需要在鏈上建立[數個資料表](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/mud.config.ts)。 + +- `Configuration`:此資料表是單例,它沒有鍵且只有單一記錄。 它用於保存遊戲設定資訊: + - `height`:地雷區的高度 + - `width`:地雷區的寬度 + - `numberOfBombs`:每個地雷區中的炸彈數量 + +- `VerifierAddress`:此資料表也是單例。 它用於保存設定的一部分,即驗證者合約 (`verifier`) 的地址。 我們可以將此資訊放在 `Configuration` 資料表中,但它是由另一個元件(伺服器)設定的,因此將其放在單獨的資料表中更容易。 + +- `PlayerGame`:鍵是玩家的地址。 資料是: + + - `gameId`:一個 32 位元組的值,是玩家正在遊玩的地圖的哈希(遊戲識別碼)。 + - `win`:一個布林值,表示玩家是否贏得了遊戲。 + - `lose`:一個布林值,表示玩家是否輸掉了遊戲。 + - `digNumber`:遊戲中成功挖掘的次數。 + +- `GamePlayer`:此資料表保存從 `gameId` 到玩家地址的反向對應。 + +- `Map`:鍵是由三個值組成的元組: + + - `gameId`:一個 32 位元組的值,是玩家正在遊玩的地圖的哈希(遊戲識別碼)。 + - `x` 座標 + - `y` 座標 + + 值是一個單一數字。 如果偵測到炸彈,則為 255。 否則,它是該位置周圍炸彈數量加一。 我們不能只使用炸彈的數量,因為在 EVM 中所有儲存空間和 MUD 中所有行值預設為零。 我們需要區分「玩家還沒有在這裡挖掘」和「玩家在這裡挖掘了,但發現周圍沒有炸彈」。 + +此外,用戶端和伺服器之間的通訊是透過鏈上元件進行的。 這也是使用資料表實作的。 + +- `PendingGame`:未處理的新遊戲開始請求。 +- `PendingDig`:在特定遊戲的特定位置挖掘的未處理請求。 這是一個[鏈下資料表](https://mud.dev/store/tables#types-of-tables),表示它不會寫入 EVM 儲存空間,只能透過事件在鏈下讀取。 + +### 執行與資料流 {#execution-data-flows} + +這些流程協調用戶端、鏈上元件和伺服器之間的執行。 + +#### 初始化 {#initialization-flow} + +當您執行 `mprocs` 時,會發生以下步驟: + +1. [`mprocs`](https://github.com/pvolok/mprocs) 會執行四個元件: + + - [Anvil](https://book.getfoundry.sh/anvil/),執行本機區塊鏈 + - [合約](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/contracts),編譯(如果需要)並部署 MUD 的合約 + - [用戶端](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/client),執行 [Vite](https://vitejs.dev/) 以向 Web 瀏覽器提供 UI 和用戶端程式碼。 + - [伺服器](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/server),執行伺服器動作 + +2. `contracts` 套件會部署 MUD 合約,然後執行 [`PostDeploy.s.sol` 指令碼](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/script/PostDeploy.s.sol)。 此指令碼會設定組態。 來自 github 的程式碼指定了[一個 10x5 的地雷區,其中有八個地雷](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/script/PostDeploy.s.sol#L23)。 + +3. [伺服器](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts) 首先[設定 MUD](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L6)。 除其他事項外,這會啟動資料同步,因此相關資料表的副本會存在於伺服器的記憶體中。 + +4. 伺服器訂閱一個函式,以便[在 `Configuration` 資料表變更時](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L23)執行。 `PostDeploy.s.sol` 執行並修改資料表後,會呼叫[此函式](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L24-L168)。 + +5. 當伺服器初始化函式取得設定後,[它會呼叫 `zkFunctions`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L34-L35) 來初始化[伺服器的零知識部分](#using-zokrates-from-typescript)。 在我們取得設定之前,這無法發生,因為零知識函式必須將地雷區的寬度和高度作為常數。 + +6. 伺服器的零知識部分初始化後,下一步是[將零知識驗證合約部署到區塊鏈](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L42-L53)並在 MUD 中設定驗證對象地址。 + +7. 最後,我們訂閱更新,這樣我們就可以看到玩家何時請求[開始新遊戲](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L55-L71)或[在現有遊戲中挖掘](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L73-L108)。 + +#### 新遊戲 {#new-game-flow} + +這是玩家請求新遊戲時發生的情況。 + +1. 如果此玩家沒有正在進行的遊戲,或者有遊戲但 gameId 為零,用戶端會顯示[新遊戲按鈕](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175)。 當使用者按下此按鈕時,[React 會執行 `newGame` 函式](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L96)。 + +2. [`newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/mud/createSystemCalls.ts#L43-L46) 是一個 `System` 呼叫。 在 MUD 中,所有呼叫都透過 `World` 合約路由,在大多數情況下,您會呼叫 `__`。 在這種情況下,呼叫的是 `app__newGame`,然後 MUD 會將其路由到 [`GameSystem` 中的 `newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L16-L22)。 + +3. 鏈上函式會檢查玩家是否沒有正在進行的遊戲,如果沒有,則[將請求新增至 `PendingGame` 資料表](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L21)。 + +4. 伺服器偵測到 `PendingGame` 中的變更,並[執行訂閱的函式](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L55-L71)。 此函式會呼叫 [`newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L110-L114),而後者又會呼叫 [`createGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L116-L144)。 + +5. `createGame` 做的第一件事是[建立一個具有適當數量地雷的隨機地圖](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L120-L135)。 然後,它會呼叫 [`makeMapBorders`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L147-L166) 來建立一個帶有空白邊界的地圖,這對於 Zokrates 是必要的。 最後,`createGame` 會呼叫 [`calculateMapHash`](#calculateMapHash) 來取得地圖的哈希,該哈希用作遊戲 ID。 + +6. `newGame` 函式將新遊戲新增至 `gamesInProgress`。 + +7. 伺服器做的最後一件事是呼叫 [`app__newGameResponse`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L38-L43),這是在鏈上進行的。 此函式位於另一個 `System` 中,即 [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol),以啟用存取控制。 存取控制在 [MUD 設定檔](https://mud.dev/config) [`mud.config.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/mud.config.ts#L67-L72) 中定義。 + + 存取清單只允許單一地址呼叫 `System`。 這將對伺服器函式的存取限制為單一地址,因此沒有人可以冒充伺服器。 + +8. 鏈上元件會更新相關資料表: + + - 在 `PlayerGame` 中建立遊戲。 + - 在 `GamePlayer` 中設定反向對應。 + - 從 `PendingGame` 中移除請求。 + +9. 伺服器識別到 `PendingGame` 的變更,但不會執行任何操作,因為 [`wantsGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L58-L60) 為 false。 + +10. 在用戶端,[`gameRecord`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L143-L148) 被設定為玩家地址的 `PlayerGame` 項目。 當 `PlayerGame` 變更時,`gameRecord` 也會變更。 + +11. 如果 `gameRecord` 中有值,且遊戲尚未分出勝負,用戶端會[顯示地圖](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175-L190)。 + +#### 挖掘 {#dig-flow} + +1. 玩家[點擊地圖儲存格的按鈕](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L188),這會呼叫 [`dig` 函式](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/mud/createSystemCalls.ts#L33-L36)。 此函式會[在鏈上呼叫 `dig`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L24-L32)。 + +2. 鏈上元件會[執行一些健全性檢查](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L25-L30),如果成功,則將挖掘請求新增至 [`PendingDig`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L31)。 + +3. 伺服器[偵測到 `PendingDig` 的變更](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L73)。 [如果有效](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L75-L84),它會[呼叫零知識程式碼](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L86-L95)(如下所述)以產生結果和其有效的證明。 + +4. [伺服器](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L97-L107) 在鏈上呼叫 [`digResponse`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L45-L64)。 + +5. `digResponse` 做兩件事。 首先,它會檢查[零知識證明](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L47-L61)。 然後,如果證明通過檢查,它會呼叫 [`processDigResult`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L67-L86) 來實際處理結果。 + +6. `processDigResult` 檢查遊戲是否已[失敗](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L76-L78)或[獲勝](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L83-L86),並[更新鏈上地圖 `Map`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L80)。 + +7. 用戶端會自動擷取更新並[更新顯示給玩家的地圖](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175-L190),並在適用的情況下告知玩家是獲勝還是失敗。 + +## 使用 Zokrates {#using-zokrates} + +在上述流程中,我們跳過了零知識部分,將其視為一個黑盒子。 現在讓我們打開它,看看程式碼是如何編寫的。 + +### 對地圖進行哈希 {#hashing-map} + +我們可以使用[此 JavaScript 程式碼](https://github.com/ZK-Plus/ICBC24_Tutorial_Compute-Offchain-Verify-onchain/tree/solutions/exercise)來實作 [Poseidon](https://www.poseidon-hash.info),這是我們使用的 Zokrates 哈希函式。 然而,雖然這樣做會更快,但它也比僅使用 Zokrates 哈希函式來做更複雜。 這是一個教學課程,因此程式碼是為簡單性而非效能而優化的。 因此,我們需要兩個不同的 Zokrates 程式,一個只計算地圖的哈希(`hash`),另一個則實際建立在地圖上某個位置挖掘結果的零知識證明(`dig`)。 + +### 哈希函式 {#hash-function} + +這是計算地圖哈希的函式。 我們將逐行檢視此程式碼。 + +``` +import "hashes/poseidon/poseidon.zok" as poseidon; +import "utils/pack/bool/pack128.zok" as pack128; +``` + +這兩行從 [Zokrates 標準程式庫](https://zokrates.github.io/toolbox/stdlib.html)匯入兩個函式。 [第一個函式](https://github.com/Zokrates/ZoKrates/blob/latest/zokrates_stdlib/stdlib/hashes/poseidon/poseidon.zok) 是 [Poseidon 哈希](https://www.poseidon-hash.info/). 它接受一個 [`field` 元素](https://zokrates.github.io/language/types.html#field)的陣列,並傳回一個 `field`。 + +Zokrates 中的欄位元素通常長度小於 256 位元,但不會少太多。 為了簡化程式碼,我們將地圖限制為最多 512 位元,並對四個欄位的陣列進行哈希處理,每個欄位只使用 128 位元。 為此,[`pack128` 函式](https://github.com/Zokrates/ZoKrates/blob/latest/zokrates_stdlib/stdlib/utils/pack/bool/pack128.zok) 會將 128 位元的陣列轉換成 `field`。 + +``` + def hashMap(bool[${width+2}][${height+2}] map) -> field { +``` + +此行開始函式定義。 `hashMap` 取得一個名為 `map` 的單一參數,它是一個二維 `bool`(ean) 陣列。 地圖的大小是 `width+2` 乘以 `height+2`,原因[如下所述](#why-map-border)。 + +我們可以使用 `${width+2}` 和 `${height+2}`,因為 Zokrates 程式在此應用程式中以[範本字串](https://www.w3schools.com/js/js_string_templates.asp)的形式儲存。 `${` 和 `}` 之間的程式碼由 JavaScript 評估,這樣程式就可以用於不同大小的地圖。 地圖參數周圍有一個寬度為一個位置的邊界,其中沒有任何炸彈,這就是我們需要將寬度和高度加二的原因。 + +傳回值是包含哈希的 `field`。 + +``` + bool[512] mut map1d = [false; 512]; +``` + +地圖是二維的。 然而,`pack128` 函式不適用於二維陣列。 所以我們先使用 `map1d` 將地圖扁平化為一個 512 位元組的陣列。 預設情況下,Zokrates 變數是常數,但我們需要在迴圈中為此陣列賦值,因此我們將其定義為 [`mut`](https://zokrates.github.io/language/variables.html#mutability)。 + +我們需要初始化陣列,因為 Zokrates 沒有 `undefined`。 `[false; 512]` 運算式表示[一個包含 512 個 `false` 值的陣列](https://zokrates.github.io/language/types.html#declaration-and-initialization)。 + +``` + u32 mut counter = 0; +``` + +我們還需要一個計數器來區分我們已在 `map1d` 中填入的位元和尚未填入的位元。 + +``` + for u32 x in 0..${width+2} { +``` + +這是在 Zokrates 中宣告 [`for` 迴圈](https://zokrates.github.io/language/control_flow.html#for-loops)的方式。 Zokrates `for` 迴圈必須有固定的邊界,因為雖然它看起來像一個迴圈,但編譯器實際上會「展開」它。 運算式 `${width+2}` 是一個編譯時常數,因為 `width` 是由 TypeScript 程式碼在呼叫編譯器之前設定的。 + +``` + for u32 y in 0..${height+2} { + map1d[counter] = map[x][y]; + counter = counter+1; + } + } +``` + +對於地圖中的每個位置,將該值放入 `map1d` 陣列並遞增計數器。 + +``` + field[4] hashMe = [ + pack128(map1d[0..128]), + pack128(map1d[128..256]), + pack128(map1d[256..384]), + pack128(map1d[384..512]) + ]; +``` + +`pack128` 從 `map1d` 建立一個包含四個 `field` 值的陣列。 在 Zokrates 中,`array[a..b]` 表示從 `a` 開始到 `b-1` 結束的陣列切片。 + +``` + return poseidon(hashMe); +} +``` + +使用 `poseidon` 將此陣列轉換為哈希。 + +### 哈希程式 {#hash-program} + +伺服器需要直接呼叫 `hashMap` 來建立遊戲識別碼。 然而,Zokrates 只能呼叫程式上的 `main` 函式來啟動,所以我們建立一個帶有呼叫哈希函式的 `main` 函式的程式。 + +``` +${hashFragment} + +def main(bool[${width+2}][${height+2}] map) -> field { + return hashMap(map); +} +``` + +### 挖掘程式 {#dig-program} + +這是應用程式零知識部分的核心,我們在這裡產生用於驗證挖掘結果的證明。 + +``` +${hashFragment} + +// The number of mines in location (x,y) +def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 { + return if map[x+1][y+1] { 1 } else { 0 }; +} +``` + +#### 為什麼需要地圖邊界 {#why-map-border} + +零知識證明使用[算術電路](https://medium.com/web3studio/simple-explanations-of-arithmetic-circuits-and-zero-knowledge-proofs-806e59a79785),它沒有與 `if` 陳述式等效的簡單方法。 取而代之的是,他們使用等效於[條件運算子](https://en.wikipedia.org/wiki/Ternary_conditional_operator)的方法。 如果 `a` 可以是零或一,您可以將 `if a { b } else { c }` 計算為 `ab+(1-a)c`。 + +因此,Zokrates `if` 陳述式總是會評估兩個分支。 例如,如果您有這段程式碼: + +``` +bool[5] arr = [false; 5]; +u32 index=10; +return if index>4 { 0 } else { arr[index] } +``` + +它會出錯,因為它需要計算 `arr[10]`,即使該值稍後會乘以零。 + +這就是我們需要在地圖周圍留一個位置寬邊界的原因。 我們需要計算一個位置周圍的地雷總數,這表示我們需要查看我們挖掘位置的上一行和下一行、左側和右側的位置。 這意味著這些位置必須存在於提供給 Zokrates 的地圖陣列中。 + +``` +def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) { +``` + +預設情況下,Zokrates 證明包含其輸入。 除非您實際知道是哪個點,否則知道某個點周圍有五個地雷是沒有用的(而且您不能只將它與您的請求匹配,因為那樣證明者就可以使用不同的值而不告訴您)。 然而,我們需要將地圖保密,同時將其提供給 Zokrates。 解決方案是使用一個 `private` 參數,一個_不_會被證明揭示的參數。 + +這開啟了另一個濫用的途徑。 證明者可以使用正確的座標,但建立一個在地點周圍有任意數量地雷的地圖,甚至可能在地點本身就有地雷。 為防止這種濫用,我們讓零知識證明包含地圖的哈希,也就是遊戲識別碼。 + +``` + return (hashMap(map), +``` + +此處的傳回值是一個元組,其中包含地圖哈希陣列以及挖掘結果。 + +``` + if map2mineCount(map, x, y) > 0 { 0xFF } else { +``` + +如果位置本身有炸彈,我們使用 255 作為特殊值。 + +``` + map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) + } + ); +} +``` + +如果玩家沒有踩到地雷,則將該位置周圍區域的地雷數量相加並傳回。 + +### 從 TypeScript 使用 Zokrates {#using-zokrates-from-typescript} + +Zokrates 有一個命令列介面,但在這個程式中,我們在 [TypeScript 程式碼](https://zokrates.github.io/toolbox/zokrates_js.html)中使用它。 + +包含 Zokrates 定義的程式庫稱為 [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts)。 + +```typescript +import { initialize as zokratesInitialize } from "zokrates-js" +``` + +匯入 [Zokrates JavaScript 繫結](https://zokrates.github.io/toolbox/zokrates_js.html)。 我們只需要 [`initialize`](https://zokrates.github.io/toolbox/zokrates_js.html#initialize) 函式,因為它會傳回一個解析為所有 Zokrates 定義的 promise。 + +```typescript +export const zkFunctions = async (width: number, height: number) : Promise => { +``` + +與 Zokrates 本身相似,我們也只匯出一個函式,這個函式也是[非同步的](https://www.w3schools.com/js/js_async.asp)。 當它最終傳回時,它會提供幾個函式,如下所示。 + +```typescript +const zokrates = await zokratesInitialize() +``` + +初始化 Zokrates,從程式庫中取得我們需要的一切。 + +```typescript +const hashFragment = ` + import "utils/pack/bool/pack128.zok" as pack128; + import "hashes/poseidon/poseidon.zok" as poseidon; + . + . + . + } + ` + +const hashProgram = ` + ${hashFragment} + . + . + . + ` + +const digProgram = ` + ${hashFragment} + . + . + . + ` +``` + +接下來是我們上面看到的哈希函式和兩個 Zokrates 程式。 + +```typescript +const digCompiled = zokrates.compile(digProgram) +const hashCompiled = zokrates.compile(hashProgram) +``` + +我們在這裡編譯這些程式。 + +```typescript +// Create the keys for zero knowledge verification. +// On a production system you'd want to use a setup ceremony. +// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). +const keySetupResults = zokrates.setup(digCompiled.program, "") +const verifierKey = keySetupResults.vk +const proverKey = keySetupResults.pk +``` + +在生產系統上,我們可能會使用更複雜的[設定儀式](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony),但對於示範來說,這已經足夠了。 使用者知道證明者金鑰不是問題——他們仍然無法用它來證明事情,除非事情是真的。 因為我們指定了熵(第二個參數 `""`),所以結果總會是相同的。 + +**注意:** Zokrates 程式的編譯和金鑰的建立是緩慢的過程。 不需要每次都重複它們,只有當地圖大小改變時才需要。 在生產系統上,您只會做一次,然後儲存輸出。 我不在這裡這樣做的唯一原因是為了簡單起見。 + +#### `calculateMapHash` {#calculateMapHash} + +```typescript +const calculateMapHash = function (hashMe: boolean[][]): string { + return ( + "0x" + + BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1)) + .toString(16) + .padStart(64, "0") + ) +} +``` + +[`computeWitness`](https://zokrates.github.io/toolbox/zokrates_js.html#computewitnessartifacts-args-options) 函式實際上會執行 Zokrates 程式。 它會傳回一個包含兩個欄位的結構:`output`,即程式的輸出,為 JSON 字串;以及 `witness`,即建立結果的零知識證明所需的資訊。 這裡我們只需要輸出。 + +輸出是一個形式為 `"31337"` 的字串,一個用引號括起來的十進位數字。 但我們需要 `viem` 的輸出是一個形式為 `0x60A7` 的十六進位數字。 所以我們使用 `.slice(1,-1)` 來移除引號,然後使用 `BigInt` 將剩餘的字串(一個十進位數字)轉換為 [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)。 `.toString(16)` 將此 `BigInt` 轉換為十六進位字串,而 `"0x"+` 則添加了十六進位數字的標記。 + +```typescript +// Dig and return a zero knowledge proof of the result +// (server-side code) +``` + +零知識證明包括公共輸入(`x` 和 `y`)和結果(地圖的哈希和炸彈的數量)。 + +```typescript + const zkDig = function(map: boolean[][], x: number, y: number) : any { + if (x<0 || x>=width || y<0 || y>=height) + throw new Error("Trying to dig outside the map") +``` + +在 Zokrates 中檢查索引是否越界是一個問題,所以我們在這裡進行檢查。 + +```typescript +const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`]) +``` + +執行挖掘程式。 + +```typescript + const proof = zokrates.generateProof( + digCompiled.program, + runResults.witness, + proverKey) + + return proof + } +``` + +使用 [`generateProof`](https://zokrates.github.io/toolbox/zokrates_js.html#generateproofprogram-witness-provingkey-entropy) 並傳回證明。 + +```typescript +const solidityVerifier = ` + // Map size: ${width} x ${height} + \n${zokrates.exportSolidityVerifier(verifierKey)} + ` +``` + +一個 Solidity 驗證器,這是一個我們可以部署到區塊鏈並用來驗證由 `digCompiled.program` 產生的證明的智能合約。 + +```typescript + return { + zkDig, + calculateMapHash, + solidityVerifier, + } +} +``` + +最後,傳回其他程式碼可能需要的一切。 + +## 安全性測試 {#security-tests} + +安全性測試很重要,因為功能錯誤最終會顯現出來。 但如果應用程式不安全,這很可能會隱藏很長一段時間,直到有人作弊並竊取屬於他人的資源時才會被揭露。 + +### 權限 {#permissions} + +在這個遊戲中,有一個特權實體,那就是伺服器。 它是唯一允許呼叫 [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol) 中函式的使用者。 我們可以使用 [`cast`](https://book.getfoundry.sh/cast/) 來驗證對權限函式的呼叫僅能以伺服器帳戶進行。 + +[伺服器的私密金鑰在 `setupNetwork.ts` 中](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/mud/setupNetwork.ts#L52)。 + +1. 在執行 `anvil`(區塊鏈)的電腦上,設定這些環境變數。 + + ```sh copy + WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b + UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + ``` + +2. 使用 `cast` 嘗試將驗證器地址設定為未經授權的地址。 + + ```sh copy + cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEY + ``` + + `cast` 不僅會報告失敗,您還可以在瀏覽器中的遊戲中開啟 **MUD 開發工具**,點擊**資料表**,然後選擇 **app\_\_VerifierAddress**。 查看地址是否不為零。 + +3. 將驗證器地址設定為伺服器的地址。 + + ```sh copy + cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY + ``` + + **app\_\_VerifiedAddress** 中的地址現在應為零。 + +所有在同一個 `System` 中的 MUD 函式都經過相同的存取控制,所以我認為這個測試是足夠的。 如果您不這麼認為,您可以檢查 [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol) 中的其他函式。 + +### 零知識濫用 {#zero-knowledge-abuses} + +驗證 Zokrates 的數學超出了本教學課程的範圍(以及我的能力)。 然而,我們可以對零知識程式碼執行各種檢查,以驗證如果它沒有正確完成就會失敗。 所有這些測試都要求我們更改 [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts) 並重新啟動整個應用程式。 僅重新啟動伺服器程序是不夠的,因為它會使應用程式處於不可能的狀態(玩家正在進行遊戲,但遊戲不再對伺服器可用)。 + +#### 錯誤答案 {#wrong-answer} + +最簡單的可能性是在零知識證明中提供錯誤的答案。 為此,我們進入 `zkDig` 並[修改第 91 行](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L91): + +```ts +proof.inputs[3] = "0x" + "1".padStart(64, "0") +``` + +這表示無論正確答案為何,我們都將始終聲稱有一個炸彈。 嘗試用這個版本玩遊戲,您會在 `pnpm dev` 畫面的 **server** 標籤中看到此錯誤: + +``` + cause: { + code: 3, + message: 'execution reverted: revert: Zero knowledge verification fail', + data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000 +000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6 +e206661696c' + }, +``` + +所以這種作弊方式會失敗。 + +#### 錯誤的證明 {#wrong-proof} + +如果我們提供正確的資訊,但證明資料錯誤會發生什麼? 現在,將第 91 行替換為: + +```ts +proof.proof = { + a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + b: [ + ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + ], + c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], +} +``` + +它仍然失敗,但現在它在沒有原因的情況下失敗,因為它發生在驗證器呼叫期間。 + +### 使用者如何驗證零信任程式碼? {#user-verify-zero-trust} + +智能合約相對容易驗證。 通常,開發者會將原始碼發佈到區塊瀏覽器,而區塊瀏覽器會驗證原始碼是否確實編譯為[合約部署交易](/developers/docs/smart-contracts/deploying/)中的程式碼。 在 MUD `System`s 的情況下,這[稍微複雜一些](https://mud.dev/cli/verify),但不會複雜太多。 + +這對零知識來說更難。 驗證器包含一些常數,並對它們執行一些計算。 這不會告訴您正在證明什麼。 + +```solidity + function verifyingKey() pure internal returns (VerifyingKey memory vk) { + vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f)); + vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]); +``` + +解決方案是,至少在區塊瀏覽器將 Zokrates 驗證新增到其使用者介面之前,應用程式開發者應提供 Zokrates 程式,並讓至少一些使用者使用適當的驗證金鑰自行編譯它們。 + +若要如此做: + +1. [安裝 Zokrates](https://zokrates.github.io/gettingstarted.html)。 + +2. 建立一個名為 `dig.zok` 的檔案,其中包含 Zokrates 程式。 以下程式碼假設您保留了原始地圖大小 10x5。 + + ```zokrates + import "utils/pack/bool/pack128.zok" as pack128; + import "hashes/poseidon/poseidon.zok" as poseidon; + + def hashMap(bool[12][7] map) -> field { + bool[512] mut map1d = [false; 512]; + u32 mut counter = 0; + + for u32 x in 0..12 { + for u32 y in 0..7 { + map1d[counter] = map[x][y]; + counter = counter+1; + } + } + + field[4] hashMe = [ + pack128(map1d[0..128]), + pack128(map1d[128..256]), + pack128(map1d[256..384]), + pack128(map1d[384..512]) + ]; + + return poseidon(hashMe); + } + + + // The number of mines in location (x,y) + def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 { + return if map[x+1][y+1] { 1 } else { 0 }; + } + + def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) { + return (hashMap(map) , + if map2mineCount(map, x, y) > 0 { 0xFF } else { + map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) + } + ); + } + ``` + +3. 編譯 Zokrates 程式碼並建立驗證金鑰。 驗證金鑰必須使用原始伺服器中使用的相同熵來建立,[在這種情況下是一個空字串](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L67)。 + + ```sh copy + zokrates compile --input dig.zok + zokrates setup -e "" + ``` + +4. 自行建立 Solidity 驗證器,並驗證其功能上與區塊鏈上的驗證器相同(伺服器會新增註解,但這不重要)。 + + ```sh copy + zokrates export-verifier + diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol + ``` + +## 設計決策 {#design} + +在任何足夠複雜的應用程式中,都有相互競爭的設計目標,需要權衡取捨。 讓我們看看一些權衡以及為什麼當前的解決方案優於其他選項。 + +### 為什麼是零知識 {#why-zero-knowledge} + +對於踩地雷遊戲,您並不需要真正的零知識。 伺服器可以一直持有地圖,然後在遊戲結束時揭示所有內容。 然後,在遊戲結束時,智能合約可以計算地圖哈希,驗證其是否匹配,如果不匹配則懲罰伺服器或完全忽略遊戲。 + +我沒有使用這個更簡單的解決方案,因為它只適用於具有明確結束狀態的短遊戲。 當遊戲可能無限進行時(例如[自主世界](https://0xparc.org/blog/autonomous-worlds)的情況),您需要一個可以在_不_揭示狀態的情況下證明狀態的解決方案。 + +作為教學課程,本文需要一個簡短且易於理解的遊戲,但此技術對於較長的遊戲最有用。 + +### 為什麼是 Zokrates? {#why-zokrates} + +[Zokrates](https://zokrates.github.io/) 並不是唯一可用的零知識程式庫,但它與正常的、[命令式](https://en.wikipedia.org/wiki/Imperative_programming)程式設計語言相似,並支援布林變數。 + +對於您的應用程式,由於有不同的需求,您可能更喜歡使用 [Circum](https://docs.circom.io/getting-started/installation/) 或 [Cairo](https://www.cairo-lang.org/tutorials/getting-started-with-cairo/)。 + +### 何時編譯 Zokrates {#when-compile-zokrates} + +在這個程式中,我們在[每次伺服器啟動時](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L60-L61)編譯 Zokrates 程式。 這顯然是浪費資源,但這是一個教學課程,以簡單為優化目標。 + +如果我正在編寫一個生產級的應用程式,我會檢查我是否擁有此地雷區大小的已編譯 Zokrates 程式檔案,如果是,就使用它。 在鏈上部署驗證器合約也是如此。 + +### 建立驗證器和證明者金鑰 {#key-creation} + +[金鑰建立](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L63-L69)是另一個純粹的計算,對於給定的地雷區大小,不需要執行多次。 同樣,為了簡單起見,它只執行一次。 + +此外,我們可以使用[設定儀式](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony)。 設定儀式的優點是,您需要每個參與者的熵或一些中間結果才能在零知識證明上作弊。 如果至少有一個儀式參與者是誠實的並刪除了該資訊,那麼零知識證明就可以免受某些攻擊。 然而,_沒有機制_可以驗證資訊是否已從所有地方刪除。 如果零知識證明至關重要,您會希望參與設定儀式。 + +在這裡,我們依賴 [perpetual powers of tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau),它有數十名參與者。 這可能足夠安全,而且簡單得多。 我們在金鑰建立過程中也不添加熵,這使得使用者更容易[驗證零知識設定](#user-verify-zero-trust)。 + +### 在哪裡驗證 {#where-verification} + +我們可以在鏈上(這會消耗 gas)或在用戶端(使用 [`verify`](https://zokrates.github.io/toolbox/zokrates_js.html#verifyverificationkey-proof))驗證零知識證明。 我選擇了前者,因為這可以讓您[驗證驗證器](#user-verify-zero-trust)一次,然後相信只要其合約地址保持不變,它就不會改變。 如果驗證是在用戶端上完成的,那麼您每次下載用戶端時都必須驗證您收到的程式碼。 + +此外,雖然這個遊戲是單人遊戲,但很多區塊鏈遊戲都是多人遊戲。 鏈上驗證意味著您只需驗證零知識證明一次。 在用戶端進行驗證將需要每個用戶端獨立驗證。 + +### 在 TypeScript 或 Zokrates 中扁平化地圖? {#where-flatten} + +一般來說,當處理可以在 TypeScript 或 Zokrates 中完成時,最好在 TypeScript 中進行,因為它快得多,且不需要零知識證明。 這就是為什麼,例如,我們不向 Zokrates 提供哈希並讓它驗證其是否正確的原因。 哈希必須在 Zokrates 內部完成,但傳回的哈希與鏈上的哈希之間的匹配可以在其外部進行。 + +然而,我們仍然[在 Zokrates 中扁平化地圖](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L15-L20),而我們本可以在 TypeScript 中完成。 原因是我認為其他選項更糟。 + +- 向 Zokrates 程式碼提供一個布林值的一維陣列,並使用 `x*(height+2) + +y` 之類的運算式來取得二維地圖。 這會讓[程式碼](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L44-L47)變得更複雜一些,所以我決定對於教學課程來說,效能的提升不值得這樣做。 + +- 向 Zokrates 傳送一維陣列和二維陣列。 然而,這個解決方案對我們沒有任何好處。 Zokrates 程式碼必須驗證它所提供的一維陣列是否真的是二維陣列的正確表示。 所以不會有任何效能提升。 + +- 在 Zokrates 中扁平化二維陣列。 這是最簡單的選項,所以我選擇了它。 + +### 地圖儲存在哪裡 {#where-store-maps} + +在這個應用程式中,[`gamesInProgress`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L20) 只是記憶體中的一個變數。 這意味著如果您的伺服器死機並需要重新啟動,它儲存的所有資訊都會遺失。 玩家不僅無法繼續他們的遊戲,他們甚至無法開始新遊戲,因為鏈上元件認為他們仍在進行遊戲。 + +對於生產系統來說,這顯然是不好的設計,在生產系統中,您會將此資訊儲存在資料庫中。 我之所以在這裡使用變數,唯一的原因是這是一個教學課程,簡單性是主要考量。 + +## 結論:在什麼條件下這是一種適當的技術? {#conclusion} + +所以,現在您知道如何編寫一個帶有伺服器的遊戲,該伺服器儲存不屬於鏈上的秘密狀態。 但您應該在什麼情況下這樣做呢? 有兩個主要考量。 + +- _長期運行的遊戲_:[如上所述](#why-zero-knowledge),在一個短遊戲中,您可以在遊戲結束後發佈狀態,然後進行驗證。 但當遊戲需要很長或不確定的時間,並且狀態需要保密時,這就不是一個選項。 + +- _可接受某些中心化_:零知識證明可以驗證完整性,即實體沒有偽造結果。 它們無法做的是確保實體仍然可用並回答訊息。 在可用性也需要去中心化的情況下,零知識證明並不是一個足夠的解決方案,您需要[多方計算](https://en.wikipedia.org/wiki/Secure_multi-party_computation)。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 + +### 致謝 {#acknowledgements} + +- Alvaro Alonso 閱讀了本文的草稿,並澄清了我對 Zokrates 的一些誤解。 + +任何剩餘的錯誤都由我負責。