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

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

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

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

1

CITA 入門

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

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

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

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

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

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

CITAHub 開源社區介紹

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

我們之前爲什麼想要做這個事情?CITA 入門和智能合約開發培訓CITA 入門和智能合約開發培訓

CITAHub 服務

CITAHub 爲會員企業輸出強有力的技術支持

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

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

CITAHub 爲會員企業對接豐富的合作項目

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

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

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

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

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

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

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

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

CITAHub 規模CITA 入門和智能合約開發培訓

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

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

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

CITA 底層架構設計

進入第二部分,我們來講一下 CITA 架構設計。CITA 入門和智能合約開發培訓

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

微服務架構

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

• RPC 服務網關:作爲整個 CITA 底層軟件,與應用層相連接的一個接口,爲應用提供 JSON-RPC 的接入;

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

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

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

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

• Network 網絡同步:各個節點之間的數據通訊,都是由這個模塊來來承接的。

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

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

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

• 好處三:採用消息總線進行通信,各個微服務可以利用異步消息簡化處理,又可以確保消息的安全、可靠送達。當與外部系統集成時,外部系統可以直接訪問消息總線,而無需 CITA 做適配。

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

交易處理CITA 入門和智能合約開發培訓

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

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

第二步:CITA 的 RPC 接受交易,通過消息總線傳遞給 Auth 進行交易驗證。

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

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

第五步:那麼, Auth 模塊返回交易 Hash 後,會通過消息總線把交易交給 Network

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

出塊處理CITA 入門和智能合約開發培訓

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

講完交易處理後,那麼,接下來講如何進行出塊處理?在這一頁的材料中,將會給大家介紹 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)相似的結構。

 *
    pragma solidity ^0.4.18;contract Wrestling {            // our code will go here}

我們需要兩個參與者,所以我們要添加兩個保存他們賬戶地址的變量(他們的公鑰),分別是 wrestler1 和 wrestler2 ,變量聲明方式如下。

    address public wrestler1;address public wrestler2;

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

    bool public wrestler1Played;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 入門和智能合約開發培訓

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

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

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

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

CITA 入門和智能合約開發培訓在該項目中使用了一個簡單的可以存儲用戶提交內容的智能合約,源碼:SimpleStore

地址:

https://github.com/citahub/first-forever-demo/blob/develop/hide/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    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 {    // We create 100 tokens (With 2 decimals, in reality it’s 1.00 token)            totalSupply = 100; // We give all the token to the msg.sender (in this case, it’s the creator of the contract)          balances[msg.sender] = 100;// With coins, don’t forget to keep track of who has how much in the smart contract, or they’ll be “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/

CITA 技術支持:

https://talk.citahub.com

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

CITA 入門和智能合約開發培訓
CITA 入門和智能合約開發培訓CITA 入門和智能合約開發培訓添加 BSN 客服 進入 BSN 開發者交流羣
CITA 入門和智能合約開發培訓
CITA 入門和智能合約開發培訓