初入智能合約測試與 pytest 自動化框架應用

「幣圈一天,人間十年。」

Dopiz Liu
OnedaySoftware
16 min readNov 16, 2022

--

虛擬貨幣和區塊鏈技術在這波疫情時代飛速地發展,許多人認為這些去中心化的概念可能會逐漸成為未來數十年互聯網的底層技術、被應用於各行各業,無論是銀行金融、零售商販甚至是行銷業務和遊戲產業。在軟體工程師圈子來說,這波熱潮也好比上個十年的人工智慧、機器學習一樣,肯定也是人盡皆知,無論是能夠學習新技術的應用或是現實面產業薪酬的部分都讓人趨之若鶩。

這篇的內容主要想以測試人員的角度,記錄自己近期測試區塊鏈、智能合約、去中心化和中心化服務交互上的想法,若有任何想法都歡迎大家提出討論。

區塊鏈中的智能合約(Smart Contract)通常都用來處理高價值的資產,諸如去中心化金融(Decentralized Finance,DeFi)應用、非同質化代幣(Non-Fungible Token,NFT)等等,智能合約中存在任何一個漏洞都會給用戶帶來巨大的風險。因此在區塊鏈的發展中最受到關注的不外乎就是「安全」的問題,在幣圈幾乎每隔一陣子就會聽到駭人聽聞的駭客攻擊或是資產被盜等等的事件,也因為區塊鏈的去中心化與不可竄改等特性,這些損失往往都是不可挽回的。因此需要各種領域知識與全面向的測試,才能夠揭露出智能合約中的錯誤並降低安全風險。

即便在測試驗證階段及早發現漏洞就能夠有效的減少資損發生,但也因為區塊鏈發展的速度實在太快,許多團隊為了搶得市場先機,必須捨去品質以換取快速發布,因而導致測試驗證的過程被壓縮,甚至是完全被忽略。

除此之外區塊鏈、智能合約或是去中心化應用程式(Decentralized Application,DAPP)與我們習以為常的軟體系統不同,區塊鏈上的應用在某些層面會造成可測試性較差,如系統邊界模糊、缺乏完善的測試工具等,因此測試人員在進行驗證工作時會遇到許多難點。

區塊鏈、智能合約的安全疑慮與難點

程式碼與智能合約的安全問題

為了吸引用戶和取得信任,通常項目方會將智能合約的程式碼開源公諸於世,但也就是這個特性暴露了原始碼及其邏輯,大幅降低駭客攻擊區塊鏈的難度。近年來也發生了好幾起大型的駭客事件,造成的資損更是天文數字。

智能合約一旦上鏈就無法下架,也不像傳統的軟體服務開發人員能夠在產品上線後持續更新、修復錯誤。即便現在已經有了可升級(Upgradeable)的智能合約、或是有的合約具有可暫停的機制,但倘若漏洞被發現,就會以很快的速度被散布、攻擊,當項目方意識到的時候往往都已經造成偌大的資損。

另外智能合約中一旦存放的資產足夠多時,就會被有心人盯上甚至攻擊,令人提心吊膽。因此出現了許多如資金歸集(資產達到閾值時,自動將部分資產轉移到其他地址進行結算彙總)、多重簽名(需要多個共同持有權限的地址進行簽章,才有辦法操作合約)等等的機制,而這些機制也需要被層層驗證以確保智能合約的安全。

錢包應用的安全與網路釣魚

大多數的用戶都會透過虛擬貨幣錢包(e.g. Metamask、Phantom)或是其他管道與區塊鏈進行互動,因此除了區塊鏈本身的安全問題外,透過這些第三方也可能存在其他的安全問題。比如說錢包應用不合理的產生或儲存用戶的私鑰,當服務器被有心人士攻擊時造成私鑰或其他機敏性資料洩漏等。(參考資料

近期也開始有非常多的惡意釣魚網站透過仿造知名網站、交易所,取得用戶的帳戶及密碼或資金後跑路;或是一些有心人士透過散布假訊息、穩賺不賠等不肖文案吸引無知的用戶以詐取錢財等,讓大家對幣圈有了先入為主的負面觀感。因此,如何求證來源是否安全,對於用戶來說極為重要。

去中心化應用相關系統的邊界模糊且複雜

以往的軟體系統不外乎就是前端應用、後端提供業務應用與資料儲存,即是 Client / Server 模式的應用,可以透過前端介面進行操作測試、也可以透過後端提供的 API 來模擬用戶行為和驗證數據的正確性。

但在區塊鏈中則是一個完全去中心化的分布式網路系統,這個系統可能橫跨了多個網路節點、多個儲存空間甚至多個國家地區,因此進行相關的應用開發就好比是和多個第三方合作,且這些第三方具有許多不可控、不易觀察的特性,非常考驗開發人員的系統設計及開發除錯能力,對於測試來說也有許多麻煩的地方。一旦測試目標不僅有一條鏈,甚至橫跨不同類型的區塊鏈,那麼對於不同鏈的性質,測試要考量的重點又各有不同,無論是共識機制(consensus)、交易性能、運作方式,且區塊鏈故障的種類和原因也很多,非常複雜也不易排查。

程式語言與資料型別

目前智能合約以 Solidity 語言為開發大宗,當然也還有其他語言正在發展中。但無論是哪種語言都仍處於非常新的階段,變動頻率非常高,不同版本間的語法可能會有差異。而在區塊鏈上的傳遞幾乎都以 Binary 的形式在運作,因此與智能合約的交互即便有 ABI(Application Binary Interface)能夠方便我們理解與應用,在開發與測試上還是要特別留意資料型別,像是邊界值、溢出,或是前端應用直接與智能合約互動時的傳參等。

另外還有成本與效能的問題,在區塊鏈中對智能合約執行任何交易或操作,都必須根據執行的複雜度計算過後付出相應的手續費(Gas Fee)。因此對於區塊鏈工程師而言,「控制成本」的這件事情在撰寫每一行程式碼的當下就要開始關注。

對領域知識的要求高

除了業務與程式語言相關的知識外,團隊成員還需要具備區塊鏈的相關知識,包含網際網路通訊、密碼學、共識機制,又或者是理解區塊鏈中的數據結構、鏈表或是節點樹等等的概念。這些內容與一般的業務邏輯不同,都是非常需要專業的領域。

除了上述所提及的要點以外,仍有非常多值得關注的地方。因此即便目前許多企業都非常積極的建設區塊鏈應用的資訊安全,區塊鏈的安全事件仍一再發生。

對智能合約進行測試

除了在開發階段,開發人員透過開發工具對合約進行單元測試或是靜態、動態分析外,以測試人員的角度通常會怎麼樣進行測試呢?這裡除了簡單分享測試的方式外,也同時想拋磚引玉希望大家能提供不一樣的方法或思維來分享如何對合約進行測試🤣。

手動測試

以測試的角度通常會先與開發進行 Code Review,了解業務邏輯實現的程式碼,也確認有沒有邏輯錯誤或是缺漏、未被實現的功能。除此之外也要檢驗程式碼的可測試性,因為在區塊鏈中的計算過程、狀態轉換其實是不易觀察的。好在智能合約能夠記錄 Event Logs,因此在一些比較複雜的需求上,會要求開發加上 Log 做追溯,以方便測試進行驗證。當然在測試鏈上驗證完畢後,也要考量是否需要移除這些 Log,避免有機敏性的業務邏輯透出。

再來將智能合約部署到測試鏈上後,通常會透過 Etherscan 或是 BscScan 等 Block Explorer and Analytics Platform 直接對合約的各個方法進行調用,驗證各種正向和負向或是邊界值的測試情境,或是與第三方錢包軟體互動時的非業務功能進行交互測試。但有時也會受限於這類平台對於輸入內容與型別的驗證,導致某些極端情境無法進行測試。

安全審計

除此之外團隊也應該聘請專業的安全審計人員,透過分析工具及其方法,針對合約的程式碼進行詳細評估,揭露安全漏洞、手續費效率以及與中心化平台的互動等方面的潛在問題,讓開發人員採取行動並進行修正。

自動化測試

也許有些人會有疑問:「智能合約測試有需要被自動化嗎?手動測試驗證過後,發布上鏈不是就不可回溯了嗎?」

的確在區塊鏈上的應用與傳統軟體在上線後仍可以持續更新、修復錯誤有所不同,但不同的測試方法會帶來不同的好處。撰寫程式碼不僅可以輔助手動測試、減少驗證各種情境時會遇到的重複動作、提升效率,在針對智能合約測試時,其實會非常關注 Code coverage,透過程式碼實現,比起手動測試更能夠驗證各種情境、甚至更多行程式碼。而現在也有許多透過代理合約來實現可更新的智能合約,如此一來就構成了「有可能會變動」、「會需要進行回歸測試」等撰寫的理由了。因此透過撰寫程式碼來對智能合約進行測試還是非常必要的。

即便智能合約能夠有邏輯的自動化在區塊鏈上運作,但足不足夠「智慧地」實現所有業務還是需要被打上個問號。又或者這個世界本來就是中心化運作著,所以現在很多的去中心化應用還是無法完全脫離中心化的服務。

也因為如此,在調用智能合約方法時,時常會看到許多傳參需要使用到中心化服務簽發的 signature、message 等參數,再從合約上進行解簽,進而導致在測試時若要顧及整個功能鏈路,必須同時整合中心化的後端服務 API 以及區塊鏈上去中心化的智能合約 ABI,才能夠更接近實際使用情境、確保整個功能的可用性及正確性。因此我們才需要拓展 API 自動化測試框架,將 Contract ABI 納入自動化測試的版圖來達成這件事。

自動化框架拓展

前篇提到了透過 pytest 來實現 API 自動化測試的框架,這篇接下來的段落會描述如何拓展這個框架、新增一些模組和機制,以達到能夠針對主流的 EVM(Ethereum Virtual Machine)兼容鏈如 Ethereum、Binance Smart Chain 等主鏈和測試鏈上的智能合約進行測試。

框架的整體結構其實和之前是一樣的,只不過多出了合約相關的配置、封裝智能合約 ABI 以及擴充共用模組功能的部分。因此看完這篇後,對其他部分有興趣的讀者也可以參考前篇的內容。

Configuration

Config 的部分和先前一樣用來存放各個服務的相關配置,包含 Base URL、對相關資料庫(e.g. Database、Redis)或外部服務的連線資訊,以及一些固定參數配置。

這次新增了針對區塊鏈建立連線的相關配置,如:Chain ID、RPC URL;和智能合約相關,如:ABI JSON 的路徑、代理合約或是真實合約在鏈上的地址等,這些參數在後面的 ABI class 都會使用到。(web3 library 會需要有 ABI schema 來定義智能合約可調用的方法。這裡實作上是透過 Etherscan 或是 BscScan 在合約的資訊中找到 ABI JSON 內容,並存成 JSON 檔案放入框架之中,並配置對應的檔案路徑。)

▲ Config File Example

Smart Contract ABI

整合和封裝 ABI 以及切分的結構與之前針對 API 的方法一致,讓後續寫測試腳本時方便撰寫和調用。

.abi
├── base_abi.py
├── gamefi
│ ├── race_abi.py
│ ├── race_abi.json
│ ├── vehicle_nft_abi.py
│ ├── vehicle_nft.json
│ └── ...
├── TOKEN
│ ├── USDC_abi.py
│ ├── USDC_abi.json
│ └── ...
└── ...

✔ BaseABI

和先前的 API 一樣,ABI 也需要一個 BaseABI 的父類別讓所有的合約 ABI class 繼承並調用。首先在建立一個可調用的合約實體時,會先與區塊鏈透過 websockets 進行連線,並告訴這個合約實體其合約地址與相關資訊,再透過其 ABI schema 得知可被調用的方法有哪些。

其中 balanceOf 方法在父類別的實現會直接取得到這條鏈上的 Native token 資產數量,如 Ethereum 就會取得到 ETH 的餘額、BSC 則會獲得的是 BNB 的餘額。但在許多測試情境上,時常要取得或使用其他幣種的餘額,這時就會建立各個 Token 的合約實體,並 overwrite 這個方法去調用各幣種合約底下的 balanceOf 方法。與之相像的是 transfer 方法,同樣在父類別只能針對 Native token 進行轉移,要對其他幣種操作的話,也需要後續在各幣種的合約實體進行 overwrite 後,調用合約底下的 approve、transfer 等方法。

而有稍微了解過區塊鏈或智能合約的應該會知道,在鏈上每一筆 transaction 都是需要被區塊確認(Transaction Confirmation)的,並不像以往 call API 時很快就能夠得到 Response。所以在調用合約方法後,勢必要等待一段時間後才會得到結果,因此就需要透過 wait 的方式,直到該筆交易被確認後得到 receipt 結果才能返回。當然 timeout 或是異步的設計也是不可少的,避免該筆交易的堵塞影響到了其他自動化測試案例的執行。

最後則是對合約方法進行 request,通常在智能合約中會有兩種類型的方法可以被調用,分別是 Read Write。其實可以簡單理解 Read 就是查詢儲存在鏈上的數據,調用此類方法並不會改變合約的數據或是任何區塊鏈的狀態,因此不需要連結錢包、也不需要付出 Gas Fee,意味著任何人都可以輕易的調用、讀取這些內容。而 Write 就相反,調用後有可能讓合約或是區塊鏈會有狀態的轉換或是交易的產生,也因此需要進行錢包簽署、打包交易。理解了之後再看程式碼,應該就能夠知道這兩個函式在實作上差異的原因了。

▲ BaseABI class Example

✔ Contract ABI

在各別合約的 ABI class 實作中,其實就是將合約的配置和連線建立好,以及將所有智能合約中可調用的方法,根據不同的類別、需要的傳參,轉換並建立成函式提供測試腳本調用。

▲ ABI class Implement Example

✔ Test Scripts

在測試腳本的部分,就能夠看到同時調用中心化後端服務與智能合約的測試案例。在設計測試案例時能夠彈性地按需求決定要測試整個功能鏈路、或只針對中心化後端服務 API 的功能、又或只對智能合約進行測試,就看測試當下的需求來進行撰寫。

特別要提的是除了前面講到,智能合約的結果返回是需要等待區塊確認以外,中心化服務本身要收到區塊確認的訊息也是非同步的,通常會透過建立類似 Scanner / Explorer 等定時任務或服務去確認區塊上的資訊來達成。因此測試腳本在驗證後端服務因應合約狀態變化而產生的異動時,無論透過其他 API 或是資料庫也需要把這個時間差考量進去。

▲ Test Script Example

在建立這些 Contract ABI class 或是 Component API class ,甚至是最基本測試腳本的程式碼時其實沒有什麼難度,但舉例來說當合約中可調用的方法一多、或是同一個專案內有多個合約需要被轉換為特定的程式碼時,就會非常耗費時間成本,甚至有時會因為人工的原因而導致出現失誤。

因此筆者就萌生了一個想法 — — 能不能夠快速地建立這些有固定格式、也有文件能夠遵循的程式碼呢?

Code Generator

為了解決上面提到的痛點,讓這些檔案自動產生肯定是最好的辦法。也因為之前有使用過 Flask + jinja2 框架渲染網頁模板來做一些簡易網站,這次就選擇透過 jinja2 來實現自動產生程式碼的解決方案。但有幾點值得注意的是:

  • 要產出這些 output 肯定要有 input 才能夠達成,而對於 Contract ABI 最好的 input 當然就是定義了 ABI scheme 的 ABI JSON;對於 Back-end API 則是可以透過現在許多團隊會使用的 OpenAPI Specification (e.g. Swagger)文件來做為輸入。
  • 網路上其實已經有許多 codegen 套件,但大多數都是根據 OpenAPI 文件產生後端特定語言、框架下的 RESTful API,而非自動化測試用的程式碼。而根據不同的測試框架、各團隊撰寫程式碼的習慣,所需要產出的內容或格式肯定不一樣,因此會有比較客製化的需求。

也因為上述第二點,這裡所設計的 CodeGenerator 針對輸入資料的處理、輸出的 template 的部分會因應各自不同的需求或遇到的痛點而有所異同,比如說後端工程師提供的 OpenAPI Spec 每個系統或服務的版本或格式都不同、或是後端服務使用 Java 實現,但自動化測試使用 python,導致命名規則(Camel v.s. Snake naming)也需要被轉換之類的。因此這個段落只會稍微描述部分的想法和實作,並以前面的自動化架構舉例,產出相應的 Contract ABI 檔案,對其他部分有興趣的讀者也可以直接參考文末的程式碼連結。

▲ Code Generation Flow

✔ CodeGenerator

首先 CodeGenerator class 需要將輸入進行預處理,包含了整理出所需的參數、資料型別的定義和轉換、並將這些內容存儲到各個 Coder 中。因為一個業務可能會輸出多份針對不同需求的檔案(e.g. 一份 OpenAPI Spec 輸入,同時需要輸出 Component API、Test Script、Configuration 多個檔案),因此可能會包含多個 Coder ,其中 Coder 定義了 Template file 和輸出的路徑。最後再透過 jinja2 逐個去 render 出各個 Coder 和 template file 定義的內容出來。

▲ CodeGenerator class Example

實際實現的 AbiCodeGenerator class 中定義了這個業務的一些方法,如:

  • _process:該業務下要產出的 Coder 有哪些。
  • _data_preprocessing:對輸入內容進行預處理,整理出期望的內容和格式供 render 時使用。
  • 其他應用函式
▲ ABI Coder & Generator Implement Example

✔ Template

最重要的其實依據需求撰寫 template file 的部分,可以看到檔案內容就如同前面提過的自動化測試所使用的 ABI class,只是其中會包含許多模板語法 {} 的內容被挖空,需要依靠前面 CodeGenerator 所處理過的資料來填入。

最後再結果輸出成檔案就完成了這次的工作!

其實一直以來在測試上所使用的技術或方法論都可以繼續使用到測試區塊鏈或其他新技術的應用之上,只是有些思維概念和關注角度的不同,對於新接觸的產業領域仍需要把眼界放得更寬、格局放得更大。但還是老話一句,「軟體品質」不是被測出來的,而是在需求規劃的時候推動醞釀、系統開發時寫入在產品 DNA 之中、測試驗證時確保品質並將其發揚光大。

以上的內容小小的總結這半年以來從零開始到變成幣圈韭菜、投入區塊鏈產業的一點想法,若有任何錯誤指正或是能夠參與討論的地方都歡迎各路大神提出😂。

Written by — Dopiz Liu
2022.11.16

--

--

Dopiz Liu
OnedaySoftware

A passionate Software Development Engineer in Test 🙂