CITA入門和智慧合約開發培訓

買賣虛擬貨幣

3月25日,BSN第二次開發者大賽正式啟動,本次大賽以“編寫基於多種底層框架的智慧合約”為主題,開發者可基於CITA等主流底層框架,結合業務場景設計、開發並部署智慧合約。

為了讓大家更好上手智慧合約開發,區塊鏈服務網路發展聯盟與CITAHub開源社羣共同推出本教程,助力開發者學習並熟悉合約開發,輕鬆應對此次大賽並拔得頭籌。

1

CITA入門

溪塔科技的研發工程師李耀榮:畢業於杭州電子科技大學計算機學院。擁有 15 年的計算機程式設計經驗,曾就職於華為,工作經歷涉及晶片設計、硬體電路板設計、編譯器軟體設計、分散式基礎軟體開發。

很高興能夠在這裡跟大家見面。今天晚上由我來跟大家分享CITA入門,今晚我想分享的內容以下4個部分:

第一部分:CITA 開源社羣介紹;

第二部分:深入講解 CITA 底層架構設計;

第三部分:CITA 底層配套的元件;

第四部分:如何快速構建一個CITA測試環境

CITAHub 開源社羣介紹

我們先進入第一部分,先簡單介紹一下我們的開源社羣:CITAHub。CITAHub 是溪塔科技發起,基於開源企業級區塊鏈核心 CITA 所構建的開源社羣。CITAHub 的使命是更好的連線產業應用方與技術開發方。區塊鏈是用來創造價值網路的工具,基於該理解,溪塔科技希望透過 CITAHub 開源技術社羣為開發價值網路提供場景,開發工具及最佳實踐。

我們之前為什麼想要做這個事情?

CITAHub 服務

CITAHub 為會員企業輸出強有力的技術支援

當前,對於產業應用方來說,由於資訊不對稱性問題,難以找到區塊鏈技術與自身業務結合點。CITAHub 透過社羣內豐富的區塊鏈企業資源可以為產業方提供多樣的技術諮詢服務;當產業方打磨好自身的區塊鏈產品後,區塊鏈節點網路部署的差異性會帶來運維壓力,CITAHub 有豐富的外掛應用及運維團隊,可以為產業方提供節點運維支援,降低成本。

對於技術團隊來說,會面臨找工具難,重複造輪子的問題,甚至還需要技術開發團隊大量的二次開發。CITAHub 整合生態中資源,為開發團隊提供外掛平臺,方便技術提供方更加簡單的開發區塊鏈專案;此外,CITAHub 還提供長期區塊鏈底層技術支援,為技術團隊解決後顧之憂。

CITAHub 為會員企業對接豐富的合作專案

對於產業方來說,即使有合適的專案想要尋求區塊鏈團隊支援時,由於缺乏行業資訊無法選擇合適團隊。CITAHub 能夠為產業方提供龐大的區塊鏈解決方案供應商生態,覆蓋眾多技術領域與渠道資源;在驗收與定價環節,CITAHub 可以為企業提供標準化合作模式。

對於技術提供方來說,技術團隊市場能力有限,專案資源較少,又缺乏大型專案資質以及合作渠道。CITAHub 彙總產業內的專案對接給合適的開發團隊,又開創了大型專案合作模式,根據大型專案不同需求進行拆分,與合作伙伴共同承接。

CITAHub 為會員企業提供知產授權服務

目前,CITAHub 已經擁有超過 60 項專利,CITA 技術相關專利超過 40 項。CITAHub 相關技術均許可會員使用。會員就 CITA 相關技術申請的發明專利,在會員有效期內均許可給甲方使用。

另外,CITAHub 建立了專利池。在專利池中,會員以自願原則將專利選擇性放入專利池內供其它 CITAHub 會員使用的,同期獲得已存在於專利池內其他專利使用的權利。

CITAHub 為會員企業提供全方位宣傳服務

CITAHub 與行業媒體社羣緊密合作,定期組織線上線下分享活動,邀請會員企業作為分享嘉賓,介紹自身企業在區塊鏈上的技術進展以及應用案例落地。透過分享,企業能夠為媒體社羣提供一手資料以供採編成文,提高媒體社羣的文章內容質量。媒體社羣也能夠從企業實踐中獲得行業發展的最新動態,透過自身平臺進行有效傳播,促進行業的健康發展。

那麼,我們希望透過構建 CITAHub 社羣來幫助大家能夠在社羣上尋找到各自所需要的關鍵資源。現在CITAHub採用的是會員機制,為會員提供豐富資源,在這裡我們也歡迎大家加入。CITAHub 規模

CITAHub 自 2019 年初成立以來,經過一年多的發展,目前已有近 100 家企業加入。其中包括招商銀行、秘猿科技、你好現在、荷月科技、加密矩陣、矢鏈科技等在內的多家核心開發企業,共同推進 CITA 研究與開發,促進社羣生態的繁榮發展;中鈔區塊鏈研究院、輕信科技、歐冶金服、志頂科技、派盾、仟金頂、秒鈦坊、法捕快、數秦科技等超過 50 家合作企業加入 CITAHub,提供不同行業的區塊鏈解決方案。

此外,包括火鳥財經、碳鏈價值、鋅連結、零壹財經等 20 多家媒體也加入到了 CITAHub 的開源生態建設中,為 CITAHub 以及社羣內企業的宣傳作出了巨大的貢獻。作為國內領先區塊鏈開源社羣,CITAHub 得到了國內各大開源技術社羣的支援,包括思否segmentfault、開源中國、CSDN、開源社等為區塊鏈技術的發展提供交流平臺。

CITA底層架構設計

進入第二部分,我們來講一下 CITA 架構設計。

微服務架構

CITA 本身是一個微服務架構。微服務是指將一個邏輯節點拆成了6個不同的微服務,另外還有一個是1個監控服務,在這裡並沒有列出,但實際上真正執行時會包括監控服務。從上圖可以看到, 6 個微服務分別:

• RPC 服務閘道器:作為整個 CITA 底層軟體,與應用層相連線的一個介面,為應用提供JSON-RPC的接入;

• Auth 交易驗證:提供交易相關合法性驗證等服務;

• Consensus 交易共識:確定一個區塊何時出塊,由誰出塊等資訊;

• Chain 鏈式儲存:提供區塊鏈儲存服務;

• Executor 合約引擎:用來執行交易的模組;

• Network 網路同步:各個節點之間的資料通訊,都是由這個模組來來承接的。

大家可以發現CITA對區塊鏈的各層邏輯的定義非常清晰。那麼,這樣設計的好處是什麼?

•好處一:可利用雲端計算基礎設施來按需提升效能。

•好處二:各個元件可獨立替換升級。

•好處三:採用訊息匯流排進行通訊,各個微服務可以利用非同步訊息簡化處理,又可以確保訊息的安全、可靠送達。當與外部系統整合時,外部系統可以直接訪問訊息匯流排,而無需 CITA 做適配。

這樣一來,對 CITA 的後續擴充套件與開發有非常大的好處的。這是一個基本的邏輯架構圖。那麼,接下來會以一個簡單的交易為例,一個交易是如何在邏輯架構圖裡進行工作的。交易處理

交易處理第一步:應用(包括 DApp、CLI 互動工具等)構造交易,並對交易進行數字簽名,然後傳送至 CITA 區塊鏈網路中。也就是說交易進入CITA網路的時候就已經是經過簽名的一個交易了。

第二步:CITA 的 RPC 接受交易,透過訊息匯流排傳遞給 Auth 進行交易驗證。

第三步:Auth 驗證透過,將交易放入交易池,並生成交易 Hash, 返回給 RPC。

第四步:當放到交易池後,就可以得到一個交易 Hash。此時,鏈就可以透過訊息匯流排把交易 Hash 再返還給使用者。所以說大家平時用CITA時,發現拿到的交易,實際上這個時候交易可能其實並沒有上鍊,你還需要透過希去查回執,才能得到交易是否已經上鍊的這個訊息。

第五步:那麼, Auth 模組返回交易Hash後,會透過訊息匯流排把交易交給 Network

第六步:Network 把交易廣播給其他節點,其他節點它也會做同樣的事情。也就是說當一個交易發到鏈上後,在正常的網路情況下,每個節點都會擁有這個節點的全量交易,就是說在這網路當中每個節點都擁有網路的所有的交易。那麼,交易有可能是透過node1進來的,也可能透過node4進來,這在網路設計上都是被允許的。出塊處理

講完交易處理後,那麼,接下來講如何進行出塊處理?在這一頁的材料中,將會給大家介紹CITA 中是如何出塊?

第一步:Consensus 根據出塊策略,當出塊時機到達時,從 Auth 的交易池中獲取一組交易。

這裡有兩個點需要強調:

1. 出塊策略是什麼?指的是 PBFT 的共識演算法。每個節點都會執行一個確定性的出塊選擇的一個演算法。

2. 出塊時機是什麼?指的是在CITA中,我們定義成 3 秒出塊。在共識中,實際上我們將3秒出塊劃分成很多階段,其實是一個複雜過程。

第二步:Consensus 將所獲取的交易打包成區塊傳遞給 Network。

第三步:Network 將區塊傳送給其它節點。所有共識節點根據共識演算法,對區塊進行共識。

值得注意的是,在前面的分享中有提到正常網路情況下,理論上講每個節點擁有全量的交易資料。所以,在第二步進行廣播交易時,實際上CITA做了最佳化。在廣播區塊時,並沒有把交易實體給模式廣播出去的,只把交易Hash廣播給節點,這樣一來會使交易package會大大降低,提高頻寬的利用率。

第四步:區塊共識完成後,Consensus 將區塊傳送給 Executor 執行。

第五步:Executor 執行完成後,將執行結果傳遞給 Chain;由 Chain 將執行結果及區塊儲存到資料庫中,出塊完成。為了達到更高的效能,我們在這一塊做了很多的工作。比方在共識階段,在提出Proposal時,就已經把提案這個塊就交給Execute去進行預執行。這樣可以讓交易執行與共識同時進行。這樣可以大大提升我交易處理效能。一般情況下,大概率提交的 Proposa 就是所出的塊。因此,到第4步時,其實這個交易已經執行完成。這樣最佳化會帶來效能提升。

如何進行節點部署?節點型別

在CITA中有兩類節點,即:

共識節點:共識節點具有出塊和投票許可權,交易由共識節點排序並打包成塊,共識完成後即被確認為合法區塊。簡單地說,共識節點就是參與出塊的節點。

普通節點:該節點可以同步區塊鏈上的交易,並且對交易進行驗證執行,同時,還可以接受 DApp 的 JSON-RPC 請求;但它並不參與共識投票,也不會主動發起區塊讓其它節點投票。普通節點除了沒有出塊和投票的許可權外,其它能與與共識節點相同。節點個數

之前經常有使用者在CITAHub論壇裡向我們提問:CITA到底要布幾個節點,是不是一定要部署4個節點才可以執行?

對於 CITA 節點個數:

• CITA 可以部署任意多個節點(包括 1 個)。

•CITA 採用的是類 PBFT 共識,具有一定的容錯能力,其容錯的節點個數(這裡講的是共識節點個數): n = (N - 1) / 3, 其中 n 與 N 都是自然數

所謂節點容錯能力指的是在區塊鏈網路中,某個共識節點出錯無法參與共識時,整個網路還是能夠正常工作。容錯節點個數,指的是能夠容忍多少個節點出錯,然後整個網路不受影。

· 在選擇容錯部署時,建議跨物理機器或跨雲平臺部署 。

CITA 元件介紹

第三部分,會簡單介紹在CITA底層鏈之上,有一些關鍵的元件來幫助大家去使用CITA和開發應用。

• CITA : 區塊鏈軟體,提供區塊鏈核心功能。

• CITA CLI : CITA 命令列互動工具。方便調測。

• SDK : CITA 區塊鏈應用開發套件,目前官方維護的 SDK 有:

–sdk-java

– sdk-js

• ReBirth : 區塊鏈資料快取器。

• Cyton : 區塊鏈錢包,管理使用者的私鑰及交易簽名。當前有兩個版本:

– Cyton-android

– Cyton-iOS

• Microscope : 區塊鏈瀏覽器。

• CITA IDE : 智慧合約開發與除錯。

• CITA Truffle Box : 智慧合約管理與測試。

• CITA Web Debugger : 網頁外掛版私鑰管理及交易簽名工具。

30 秒構建測試環境

接下來給大家演示如何快速搭建 CITA 的測試環境。為了方便新使用者快速使用CITA做成了一個docker映象。具體操作請檢視影片教學:http://kb.bsnbase.com/webdoc/view/Pub4028813e711a7c3901719cc6ff637f3d.html。

1. 新建 CITA 配置:

docker run -v "`pwd`":/opt/cita-run cita/cita-ce:20.2.0-secp256k1-sha3 cita create --super_admin "0x37d1c7449bfe76fe9c445e626da06265e9377601" --nodes "127.0.0.1:4000"

2. 啟動 CITA :

docker run -d -p 1337:1337 -v "`pwd`":/opt/cita-run cita/cita-ce:20.2.0-secp256k1-sha3 /bin/bash -c 'cita setup test-chain/0 && cita start test-chain/0 && sleep infinity’

3. 啟動 CITA-CLI, 進行基本操作 :

啟動 CITA-CLI:

docker run -it cita/cita-ce-cli:20.2.2

查詢塊高:

cita> rpc blockNumber{  "id": 1,  "jsonrpc": "2.0",  "result": "0x143"}

2

CITA智慧合約開發

智慧合約歷史

1994年,電腦科學家和密碼學家 Nick Szabo 首次提出“智慧合約”概念。它早於區塊鏈概念的誕生。Szabo 描述了什麼是“以數字形式指定的一系列承諾,包括各方履行這些承諾的協議”。雖然有它的好處,但智慧合約的想法一直未取得進展——主要是缺乏可以讓它發揮出作用的區塊鏈。

直到 2008 年,第一個加密貨幣比特幣才出現,同時引入了現代區塊鏈技術。區塊鏈最初是以比特幣的底層技術出現的,各種區塊鏈分叉導致發生很大的變化。智慧合約在 2008 年依然無法融入比特幣區塊鏈網路,但在五年後,以太坊讓它浮出水面。從此,湧現出了各種不同形式的智慧合約,其中以太坊智慧合約使用最廣。

自以太坊開始,區塊鏈是一個執行著智慧合約的分散式平臺:應用程式可以按照程式執行,不存在故障、審查、欺詐或第三方干預的可能性。智慧合約給予了我們使用區塊鏈技術來驗證我們執行的程式碼的執行情況的能力。智慧合約定義

智慧合約(英語:Smart contract )是一種旨在以資訊化方式傳播、驗證或執行的計算機協議。智慧合約允許在沒有第三方的情況下進行可信交易,這些交易可追蹤且不可逆轉。

智慧合約簡單定義就是智慧合約是可以處理 token 的指令碼,圍繞它可以發行,轉移和銷燬資產。這裡說的資產是一個泛化的定義,不一定是幣,可以是任何一種虛擬物品(比如應收,支付資訊甚至加密貓)和現實世界的物品在區塊鏈上的對映(比如艙單,抵押)。CITA 智慧合約

CITA 區塊鏈框架使用的虛擬機器 CITA-VM 和 EVM 採取同樣的指令集,所以合約所使用的語言也是 solidity。由於 Ethereum 是目前全球最廣泛的區塊鏈網路,所以 solidity 也是使用最廣泛的智慧合約語言,圍繞它的生態是非常豐富的,包括了合約除錯,部署工具和保護合約安全的一些庫。

這裡再談一下合約是由誰來執行的問題,在公鏈上,比如比特幣或者以太坊,這些合約由我們稱為“礦工”的參與方強制執行和證明。礦工其實是多臺電腦(也可以稱為礦機),它們把一項交易(執行智慧合約,代幣轉賬等) 以區塊的形式新增到一個公開分賬本上。使用者給這些礦工支付 “Gas”也就是手續費,它是執行一份合約的成本。

由於 CITA 是針對於企業的開放許可鏈框架,在 CITA 中礦工是出塊節點,使用智慧合約所需要的手續費是支付給出塊節點的, gas 在這裡叫做 quota。當然這裡支付比例是可以自定義調整的,具體可以見文件。同時 CITA 可以調節為無幣模式,在無幣模式下,不存在手續費。

智慧合約開發

現在,我們開始智慧合約的開發部分,Solidity 與 Javascript 很接近,但它們並不相同。而且不能在一段程式碼上強加 JQuery,智慧合約是無法呼叫區塊鏈體系之外的程式碼的。同時還有一個特點是,你在開發的時候需要特別注意安全性,因為在區塊鏈上的交易是不可逆的。智慧合約定義

透過一個例子說明基本語法,這裡參考了 ethfans 上的一個例子,如果難以理解的話可以換一個,使用當時 PeckShield 講的一個分餅乾的例子。

現在,關於我們的第一個例子,我正在考慮一個由電影《時間規劃局》啟發的指令碼。電影中,人們生活在一個反烏托邦式的未來,改用時間作為貨幣流通。他們可以透過掰手腕的方式贏取對手的時間(他們的“手臂”上儲存著時間,輸方的時間將會傳送給贏家),我們也可以這麼做!用智慧合約以角力( Wrestling )的方式賺錢。

首先,solidity 指令碼的基礎是下面這段程式碼,pragma 指明正在使用的 Solidity 版本。Wrestling 是合約的名稱,是一種與 Javascrip 上的類(class)相似的結構。

pragmasolidity^0.4.18;contract Wrestling {            // our code will go here}

我們需要兩個參與者,所以我們要新增兩個儲存他們賬戶地址的變數(他們的公鑰),分別是 wrestler1 和 wrestler2 ,變數宣告方式如下。

addresspublicwrestler1;address public wrestler2;

在我們的小遊戲中,每一輪的比賽,參與者都可以投入一筆錢,如果一個人投入的錢是另一個人的兩倍(總計),那他就贏了。定義兩個玩家是否已經投入的flag wrestler1Played 和 wrestler2Played 以及兩位玩家投入的金額 wrestler1Deposit 和 wrestler1Deposit。

boolpublicwrestler1Played;bool public wrestler2Played;uint private wrestler1Deposit;uint private wrestler2Deposit;

還有判斷遊戲結束與否,贏家和收益的變數。

bool public gameFinished; address public theWinner;uint gains;

下面介紹一些關於公鑰/私鑰的規則,在區塊鏈上每一個賬戶都是一對公私鑰,私鑰可以對一個資訊進行簽名,從而使這條資訊可以被他人驗證,被驗證的時候它的公鑰需要被使用到。在整個簽名和驗證的過程中,沒有資訊是加密的,實際上任何資訊都是公開課查驗的。

對於合約裡面的變數,本質上來講,也是可以被公開訪問的。在這裡要注意是的,即使一個變數是私有的,並不是說其他人不能讀取它的內容,而是意味著它只能在合約中被訪問。但實際上,由於整個區塊鏈儲存在許多計算機上,所以儲存在變數中的資訊總是可以被其他人看到,這是在區塊鏈中一個很重要額原則。

另一方面,和很多程式語言很像,編譯器會自動為公共變數建立 getter 函式。為了使其他的合約和使用者能夠更改公共變數的值,通知也需要針對不同的變數建立一個 setter 函式。

現在我們將為遊戲的每一步新增三個事件。

1. 開始,參與者註冊;

2. 遊戲期間,登記每一輪賽果;

3. 最後,其中一位參與者獲勝。

事件是簡單的日誌,可以在分散式應用程式(也稱為 dapps)的使用者介面中呼叫 JavaScript 回撥函式。在開發過程中,事件甚至可以用於除錯的目的,因為不同於 JavaScript 有console.log() 函式,solidity 中是沒有辦法在 console 中列印出資訊的。程式碼如下:

event WrestlingStartsEvent(address wrestler1, address wrestler2);event EndOfRoundEvent(uint wrestler1Deposit, uint wrestler2Deposit);event EndOfWrestlingEvent(address winner, uint gains);

現在我們將新增建構函式,在 Solidity 中,它與我們的合約具有相同的名稱,並且在建立合約時只呼叫一次。在這裡,第一位參與者將是創造合約的人。msg.sender 是呼叫該函式的人的地址。

function Wrestling() public {  wrestler1 = msg.sender;}

接下來,我們讓另一個參與者使用以下函式進行註冊:

function registerAsAnOpponent() public {        require(wrestler2 == address(0));    wrestler2 = msg.sender;    WrestlingStartsEvent(wrestler1, wrestler2);}

Require 函式是 Solidity 中一個特殊的錯誤處理函式,如果條件不滿足,它會回滾更改。在我們的示例中,如果變數參與者2等於0x0地址(地址等於0),我們可以繼續;如果參與者2的地址與0x0地址不同,這就意味著某個玩家已經註冊為對手,所以我們會拒絕新的註冊。可以把它認為是 solidity 中的 if() {} else{} 條件判斷。

再次強調, msg.sender 是呼叫該函式的帳戶地址,並且當我們觸發一個事件,就標誌著角力的開始。

現在,每一個參與者都會呼叫一個函式, wrestle() ,並投入資金。如果雙方已經玩過這場遊戲,我們就能知道其中一方是否獲勝(我們的規則是其中一方投入的資金必須是另一方的雙倍)。關鍵字 payable 意味著函式可以接收資金,如果它不是集合,函式則不會接受幣。msg.value 是傳送到合約中的幣的數量。

function wrestle() public payable {                require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2));        if(msg.sender == wrestler1) {                            require(wrestler1Played == false);                            wrestler1Played = true;                            wrestler1Deposit = wrestler1Deposit + msg.value;                } else {                           require(wrestler2Played == false);                           wrestler2Played = true;                           wrestler2Deposit = wrestler2Deposit + msg.value;                }                if(wrestler1Played && wrestler2Played) {                           if(wrestler1Deposit >= wrestler2Deposit * 2) {                                         endOfGame(wrestler1);                            } else if (wrestler2Deposit >= wrestler1Deposit * 2) {                         endOfGame(wrestler2);                           } else {                                     endOfRound();                           }               }    }

請注意,我們不是直接把錢交給贏家,在此情況下這並不重要,因為贏家會把該合約所有的錢提取出來;而在其他情況下,當多個使用者要把合約中的以太幣提取出來,使用 withdraw 模式會更安全,可以避免重入,在合約安全部分我們會詳細討論這些情況。

簡單地說,如果多個使用者都可以從合約中提取資金,那麼任誰都能一次性多次呼叫 withdraw 函式並多次得到報酬。所以我們需要以這樣一種方式來編寫我們的取款功能:在他繼續得到報酬之前,他應得的數額會作廢。

它看起來像這樣:

function withdraw() public {                require(gameFinished && theWinner == msg.sender);        uint amount = gains;        gains = 0;               msg.sender.transfer(amount);    }

程式碼段連結:

https://github.com/devzl/ethereum-walkthrough-1/blob/master/Wrestling.sol智慧合約的 IDE

在區塊鏈技術中,不僅轉賬是一筆交易,對合約中函式的呼叫和合約的部署都是以傳送交易的方式完成。整個過程比較繁瑣,正如同其他的變成語言一樣,針對於 solidity 智慧合約,我們也提供了 IDE (CITA IDE) 來編譯和部署合約。

CITA 的 IDE

CITA IDE 是基於 Ethereum 的 Solidity 編輯器進行修改並適配了 CITA ,是面向 CITA 的智慧合約編輯器,能夠編寫、編譯、debug、部署智慧合約。可直接執行官方 CITA IDE 1(https://cita-ide.citahub.com) 進行體驗。

使用說明

•browser 內建常用的模板合約,首先從內建合約模板中選擇合適的模板開始開發

•Compile 本地編譯,選擇當前 solidity 版本,與合約 pragma 一致

•進入右側的 Run 標籤, 在 Deploy to CITA 中填入相關資訊

– 勾選 Auto ValidUntilBlock 則傳送交易前會自動更新 validUntilBlock 欄位

– 勾選 store ABI on chain 則會在合約部署成功後將合約 ABI 儲存到 CITA 上

– 此處特別注意 Quota 的設定, 一般合約需要較多 Quota, 若 quota 不足, 在交易資訊列印的時候可以檢視 Error Message 獲知

•點選 Load Contracts 載入當前編譯完成的合約, 並選擇要部署的合約

•點選 Deploy to CITA 發起部署合約的交易

•觀察控制檯的輸出, 交易詳細資訊會顯示在控制檯上, 當流程結束時, 會輸出交易 hash 和合約地址, 並且以連結形式支援到 Microscope 檢視DApp 及智慧合約開發例項

First Forever 是一個DApp demo,展示了在 CITA 上開發一個最小可用的 DApp 的完整流程

FIrst Forever 地址:

https://github.com/citahub/first-forever-demo/blob/develop/README-CN.md

以下是區塊鏈DApp的開發步驟示意圖:

在該專案中使用了一個簡單的可以儲存使用者提交內容的智慧合約,原始碼:SimpleStore

地址:

https://github.com/citahub/first-forever-demo/blob/develop/src/contracts/SimpleStore.sol

更詳細的介紹看:如何動手做一個DApp 地址:

https://github.com/citahub/first-forever-demo/blob/develop/README-CN.md

智慧合約安全性

因為智慧合約是不可逆的,所以他的交易一旦形成,是無法回退的。在這種情形下,智慧合約的安全性尤為重要。以下先介紹幾種合約常見的合約安全性隱患,然後會給出改善他們的方法。

參考影片 :

https://www.bilibili.com/video/av58299098智慧合約溢位型漏洞

16bit 整數:0x0000,0x0001,0x0002,…,0xfffd,0xffff

0x8000 + 0x8000 = 0x10000 = 0x0000 = 0

0xffff + 0x0003 = 0x10002 = 0x0002 = 2

0x0000 - 0x0001 = 0xffff = -1 = 65535

function transferMulti(address[] _to, uint256[] _value) public returns (uint256 amount) {      require(_to.length == _value.length);      for(uint8 j; j<len; j++) {        amount += _value[j];      }      require(balanceOf[msg.sender] >= amount);      for(uint8 i ; i < len; i++) {            address _toI = _to[i];            uint256 _valueI = _value[i];            balanceOf[_toI] += _valueI;            balanceOf[msg.sender] -= _valueI;            Transfer(msg.sender, _tiI, _valueI);      }}

這個函式想要做到的是把 msg.sender 在合約中的 token 轉給多個人, amount += _value[j]; 這個操作會存在溢位的風險,如果在加的時候出現狀況 amount = 0x8000 + 0x8000 = 0,那麼在後面一步的判斷 require(balanceOf[msg.sender] >= amount);中會出現的實際判斷的是 balanceOf[msg.sender] >= 0 那麼可以從空的賬戶中把錢轉出。程式碼注入漏洞

function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) returns (bool success) {    allowed[msg.sender][_spender] = _value;    Approval(msg.sender, _spender, _value);      if(!_spender.call(_extraData)) {        revert();    }      return true;}

可以把這個合約本身擁有的代幣偷走轉給別的使用者,因為對於extraData 來說,自由度非常高, _spender.call(_extraData) 可以是任何一個地址呼叫任何一個函式。itchyDAO in MakerDAO 投票系統

這個主要是以一個比較複雜的例子來給學員講合約中函式呼叫需要知道的地方,暗示智慧合約還是比較難以把控的,需要多學習

以下是一個在 MakerDAO 中的投票系統,在這個投票系統中,一個sender 需要根據自己的權重對一個提案進行投票。

function etch(address[] memory yays) public note returns (bytes32 slate){      require(yays.length <= MAX_YAYS);  requireByOrderSet(yays);        bytes32 hash = keccak256(abi.encodePacked(yays));      emit Etch(hash);      return hash;}function vote(address[] memory yays) public returns (bytes32){      bytes32 slate = etch(yays);  vote(slate);      return slate;}function vote(bytes32 slate) public note {      uint weight = deposit[msg.sender];      subWeight(weight, vote[msg.sender]);      votes[msg.sender] = slate;      addWeight(weight, vote[msg.sender]);  }

以下是投票函式,在投票以後把票數進行 addWeight 和 subWeight 操作。

function addWeight(uint weight, bytes32 slate) internal {    address[] storage yays = slates[slate];    for(uint i = 0; i < yays.lenght; i++) {        approvals[yays[i]] = add(approvals[yays[i]], weight);    }}function subWeight(uint weight, bytes32 slate) internal {    address[] storage yays = slates[slate];    for(uint i = 0; i < yays.length; i++) {        approvals[yays[i]] = sub(approvals[yays[i]], weight);    }}

最後一步是在 lock 一種幣,在 lock 以後可以進行投票操作,在投票完成以後,可以 free 從而退回自己的幣。

function lock(uint wad) public note{      GOV.pull(msg.sender,wad);      IOU.mint(msg.sender, wad);      deposits[msg.sender] = add(deposits[msg.sender], wad);      addWeight(wad, votes[msg.sender]);}function free(uint wad) public note{      deposits[msg.sender] = sub(deposits[msg.sender], wad);      subWeight(wad, votes[msg.sender]);      IOU.burn(msg.sender, wad);      GOV.push(msg.sender, wad);}

智慧合約場景

長遠看,遵循標準有很多不應忽視的益處。首先,如果遵照某個標準生成代幣,那麼每個人都會知道該代幣的基礎功能,並知道如何與之互動,因此就會有更多信任。去中心化程式(DApps)可以直接辨別出其代幣特徵,並透過特定的 UI 來與其打交道。另外,一種代幣智慧合約的標準實現已經被社羣開發出來,它採用類似 OpenZeppelin 的架構。這種實現已經被很多大神驗證過,可以用來作為代幣開發的起點。

本文中會從頭開始提供一個不完整的,但是遵循 ERC20 標準的,基礎版的代幣實現,然後將它轉換成遵循 ERC721 標準的實現。這樣就能讓讀者看出兩個標準之間的不同。

出發點是希望大家瞭解代幣是如何工作的,其過程並不是一個黑箱;另外,對於 ERC20 這個標準,儘管它至少已經被廣泛接受兩年以上,如果只是從標準框架簡單地生成自己的代幣,也還會存在某些不易發現的故障點。ERC20 標準

ERC20(https://theethereum.wiki/w/index.php/ERC20_Token_Standard)是為同質(Fungible)代幣標準設立的標準,可以被其它應用(從錢包到去中心化交易所)重複使用。同質意味著可以用同類的代幣互換,換句話說,所有的代幣都是等價的(就像錢幣,某一美金和其它美金之間沒有區別)。而一個非同質代幣(Non-fungible Token)代表一種特定價值(例如房屋,財產,藝術品等)。同質代幣有其內在價值,而非同質代幣只是一種價值智慧合約的代表。

要提供符合ERC20標準的代幣,需要實現如下功能和事件:

contract ERC20Interface {        function totalSupply() public constant returns (uint);       function balanceOf(address tokenOwner) public constant returns (uint balance);        function allowance(address tokenOwner, address spender) public constant returns (uint remaining);        function transfer(address to, uint tokens) public returns (bool success);        function approve(address spender, uint tokens) public returns (bool success);        function transferFrom(address from, address to, uint tokens) public returns (bool success);        event Transfer(address indexed from, address indexed to, uint tokens);       event Approval(address indexed tokenOwner, address indexed spender, uint tokens);}

標準不提供功能的實現,這是因為大家可以用自己喜歡的方式寫出任何程式碼,如果不需要提供某些功能只需要按照標準(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md)返回 null/false 的值就可以了。

注意:這裡並不很強調程式碼,大家只需瞭解內部機理,全部程式碼將會在文末附上鍊接。

實現

首先,需要給代幣起一個名字,因此會採用一個公有變數(Public Variable):

string public name = “Our Tutorial Coin”;

然後給代幣起一個代號:

string public symbol = “OTC”;

當然還要有具體小數位數:

uint8 public decimals = 2;

因為 Solidity 並不完全支援浮點數,因此必須把所有數表示成整數。例如,對於一個數字 “123456”,如果使用 2 位小數,則代表 “1234.56”;如果採用4位小數,則代表 “12.3456”。0 位小數代表代幣不可分。而以太坊的加密幣以太幣則使用18位小數。一般地,代幣不需要使用18位小數,因為遵循了以太坊的慣例,也沒有什麼特別的目的。

你需要統計一共發行了多少代幣,並跟蹤每人擁有多少:

uint256 public totalSupply;mapping(address => uint256) balances;

當然,你需要從0個代幣開始,除非在代幣智慧合約建立時候就生成了一些,如下例:

// The constructor function of our Token smart contract  function TutoCoin() public {    //Wecreate100tokens(With2decimals,inrealityit’s1.00token)totalSupply=100;//Wegiveallthetokentothemsg.sender(inthiscase,it’sthecreatorofthecontract)balances[msg.sender]=100;//Withcoins,don’tforgettokeeptrackofwhohashowmuchinthesmartcontract,orthey’llbe“lost”.}

totalSupply() 函式只是從 totalSupply 變數中獲取數值:

function totalSupply() public constant returns (uint256 _totalSupply) {        return totalSupply;}balanceOf()## 也類似:// Gets the balance of the specified address.function balanceOf(address tokenOwner) public view returns (uint256 balance) {         return balances[tokenOwner];}

接下來就是ERC20的神奇之處了, transfer() 函式是將代幣從一個地址傳送到另外一個地址的函式:

function transfer(address _to, uint256 _value) public returns (bool) {         // avoid sending tokens to the 0x0 address         require(_to != address(0));         // make sure the sender has enough tokens         require(_value <= balances[msg.sender]);       // we substract the tokens from the sender’s balance         balances[msg.sender] = balances[msg.sender] - _value;         // then add them to the receiver         balances[_to] = balances[_to] + _value;       // We trigger an event, note that Transfer have a capital “T”, it’s not the function itself with a lowercase “t”        Transfer(msg.sender, _to, _value);       // the transfer was successfull, we return a true         return true;}

以上基本就是 ERC20 代幣標準的核心內容。

鑑於 ERC20 還存在其他一些問題,更安全容錯的 transferFrom() 實現和其它方案被髮布出來(如之前所說,該標準只是一些功能原型和行為定義,具體細節則靠開發者自己實現),並正在討論中,其中就包括

ERC223(https://github.com/ethereum/EIPs/issues/223) ERC777(https://github.com/ethereum/EIPs/issues/777)

ERC223 方案的動機是避免將代幣傳送到錯誤地址或者不支援這種代幣的合約上,成千上萬的金錢因為上述原因丟失,這一需求作為以太坊後續開發功能的第 223 條記錄第 223 條記錄在案。ERC777 標準在支援其它功能的同時,對接收地址進行“即將收到代幣”的提醒功能,ERC777 方案看起來很有可能替代 ERC20.ERC721 標準

ERC721目前看,ERC721 跟 ERC20 及其近親系列有本質上的不同。ERC721 中,代幣都是唯一的。ERC721 提出來後的眾多使用案例中,CryptoKitties,這款使用ERC721標準實現的收集虛擬貓遊戲使得它備受矚目。以太貓遊戲實際就是智慧合約中的非同質代幣 (non-fungible token),並在遊戲中用貓的形象來表現出來。

如果想將一個 ERC20 合約轉變成 ERC721 合約,我們需要知道 ERC721 是如何跟蹤代幣的。在 ERC20 中,每個地址都有一個賬目表,而在 ERC721 合約中,每個地址都有一個代幣列表:

mapping(address => uint[]) internal listOfOwnerTokens;

由於 Solidity 自身限制,不支援對佇列進行 indexOF() 的操作,我們不得不手動進行佇列代幣跟蹤:

mapping(uint => uint) internal tokenIndexInOwnerArray;

當然可以用自己實現的程式碼庫來發現元素的索引,考慮到索引時間有可能很長,最佳實踐還是採用對映方式。

為了更容易跟蹤代幣,還可以為代幣的擁有者設定一個對映表:

mapping(uint => address) internal tokenIdToOwner;

以上就是兩個標準之間最大的不同,ERC721 中的 transfer() 函式會為代幣設定新的擁有者:

function transfer(address _to, uint _tokenId) public (_tokenId){    // we make sure the token exists    require(tokenIdToOwner[_tokenId] != address(0));    // the sender owns the token    require(tokenIdToOwner[_tokenId] == msg.sender);    // avoid sending it to a 0x0  require(_to != address(0));    // we remove the token from last owner list    uint length = listOfOwnerTokens[msg.sender].length;    // length of owner tokens    uint index = tokenIndexInOwnerArray[_tokenId];    // index of token in owner array    uint swapToken = listOfOwnerTokens[msg.sender][length - 1];    // last token in array    listOfOwnerTokens[msg.sender][index] = swapToken;    // last token pushed to the place of the one that was transferred    tokenIndexInOwnerArray[swapToken] = index;    // update the index of the token we moved    delete listOfOwnerTokens[msg.sender][length - 1];    // remove the case we emptied    listOfOwnerTokens[msg.sender].length—;    // shorten the array’s length    // We set the new owner of the token    tokenIdToOwner[_tokenId] = _to;    // we add the token to the list of the new owner    listOfOwnerTokens[_to].push(_tokenId);    tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_to].length - 1;    Transfer(msg.sender, _to, _tokenId);}

儘管程式碼比較長,但卻是轉移代幣流程中必不可少的步驟。

還必須注意,ERC721 也支援 approve() 和 transferFrom() 函式,因此我們必須在 transfer 函式內部加上其它限制指令,這樣一來,當某個代幣有了新的擁有者,之前的被授權地址就無法其代幣進行轉移操作,程式碼如下:

function transfer(address _to, uint _tokenId) public (_tokenId){      // …     approvedAddressToTransferTokenId[_tokenId] = address(0);}

挖礦基於以上兩種標準,可能面對同一種需求,要麼產生同質代幣,要麼產生非同質代幣,一般都會用一個叫做 Mint() 的函式完成。

實現以上功能函式的程式碼如下:

function mint(address _owner, uint256 _tokenId) public (_tokenId){      // We make sure that the token doesn’t already exist      require(tokenIdToOwner[_tokenId] == address(0));    // We assign the token to someone      tokenIdToOwner[_tokenId] = _owner;      listOfOwnerTokens[_owner].push(_tokenId);      tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_owner].length - 1;    // We update the total supply of managed tokens by this contract      totalSupply = totalSupply + 1;    // We emit an event      Minted(_owner, _tokenId);}

用任意一個數字產生一個新代幣,根據不同應用場景,一般在合約內部只會授權部分地址可以對它進行鑄幣(mint)操作。

這裡需要注意 mint() 函式並沒有出現在協議標準定義中,而是我們新增上去的,也就是說我們可以對標準進行擴充,新增其它對代幣的必要操作。例如,可以新增用以太幣來買賣代幣的系統,或者刪除不再需要代幣的功能。

CITA 原始碼地址:

https://github.com/citahub

CITA 配套工具鏈:

https://www.citahub.com/#componentArea

CITA 技術支援:

https://talk.citahub.com

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;