https://segmentfault.com/a/1190000015295148
流程
合約代碼編寫(Solidity)-> 合約編譯(solc)-> 合約部署(web3)
開發語言及工具:
- 區塊鏈節點:ganache-cli
- 基礎環境:node
- 合約開發語言:Solidity
- 合約編譯器:solc
- 合約訪問庫:web3.js
基礎環境安裝
- 1、安裝 node.js
- 2、安裝 ganache-cli
ganache-cli
sudo npm install -g ganache-cli
ganache 默認會自動創建 10 個賬戶,每個賬戶有 100 個以太幣(ETH:Ether)。 可以把賬戶視為銀行賬戶,以太幣就是以太坊生態系統中的貨幣。
面輸出的最后一句話,描述了節點仿真器的監聽地址和端口為localhost:8545,在使用 web3.js 時,需要傳入這個地址來告訴web3js庫應當連接到哪一個節點。
合約設計
合約中的屬性用來聲明合約的狀態,而合約中的方法則提供修改狀態的訪問接口。
重點:
- 合約狀態是持久化到區塊鏈上的,因此對合約狀態的修改需要消耗以太幣。
- 只有在合約部署到區塊鏈的時候,才會調用構造函數,并且只調用一次。
- 與 web 世界里每次部署代碼都會覆蓋舊代碼不同,在區塊鏈上部署的合約是不可改變的,也就是說,如果你更新 合約并再次部署,舊的合約仍然會在區塊鏈上存在,并且合約的狀態數據也依然存在。新的部署將會創建合約的一 個新的實例。
狀態變量和整數
Solidity
顯著特點:
- 后綴.sol
- 強類型語言
- 語法和javascript類似
函數類型
view: 讀取區塊鏈上的數據,但是不修改區塊鏈上面的數據
pure:不修改也不讀取區塊鏈上面的數據
一筆事務的控制臺信息
地址類型
合約代碼編寫(Solidity)-> 合約編譯(solc)-> 合約部署(web3)
開發語言及工具:
- 區塊鏈節點:ganache-cli
- 基礎環境:node
- 合約開發語言:Solidity
- 合約編譯器:solc
- 合約訪問庫:web3.js
版本指令
標明 Solidity 編譯器的版本. 以避免將來新的編譯器可能破壞你的代碼。
pragma solidity ^0.4.19;
^0.4.20 0.4.x
~0.4.20 0.x.x
EVM數據存儲
storage:狀態變量(全局變量)會存儲在這里。永久性存儲數據(因為會把數據寫入區塊鏈當中),
stack:函數中的本地變量(局部變量)默認會存儲在這里,暫時性存儲數據。
memory:暫時性存儲數據,實參(函數中實際傳遞的參數)默認會存儲在這里。
tips:storage和memory都需要消耗gas,但是storage更貴。
結構體作為函數參數,函數必須是internal類型
數組
// 固定長度為2的靜態數組:
uint[2] fixedArray;
// 固定長度為5的string類型的靜態數組:
string[5] stringArray;
// 動態數組,長度不固定,可以動態添加元素:
uint[] dynamicArray;
公共數組
你可以定義 public 數組, Solidity 會自動創建 getter 方法. 語法如下:
Person[] public people;
其它的合約可以從這個數組讀取數據(但不能寫入數據),所以這在合約中是一個有用的保存公共數據的模式。
定義函數
function eatHamburgers(string _name, uint _amount) {
}
注:: 習慣上函數里的變量都是以(_)開頭 (但不是硬性規定) 以區別全局變量。我們整個教程都會沿用這個習慣。
私有 / 公共函數
Solidity 定義的函數的屬性默認為公共。 這就意味著任何一方 (或其它合約) 都可以調用你合約里的函數。
顯然,不是什么時候都需要這樣,而且這樣的合約易于受到攻擊。
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
返回值
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
函數的修飾符****view,returns
把函數定義為 view, 意味著它只能讀取數據不能更改數據:
function sayHello() public view returns (string) {}
Solidity 還支持 pure 函數, 表明這個函數甚至都不訪問應用里的數據,例如:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
Keccak256 和 類型轉換
Ethereum 內部有一個散列函數keccak256,它用了SHA3版本。一個散列函數基本上就是把一個字符串轉換為一個256位的16進制數字。字符串的一個微小變化會引起散列數據極大變化。
注: 在區塊鏈中安全地產生一個隨機數是一個很難的問題, 本例的方法不安全,但是在我們的Zombie DNA算法里不是那么重要,已經很好地滿足我們的需要了。
事件
事件 是合約和區塊鏈通訊的一種機制。你的前端應用“監聽”某些事件,并做出反應。
// 這里建立事件
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
//觸發事件,通知app
IntegersAdded(_x, _y, result);
return result;
}
YourContract.IntegersAdded(function(error, result) {
// 干些事
}
映射(****Mapping)和地址(Address)
Addresses(地址)
以太坊區塊鏈由 account (賬戶)組成,你可以把它想象成銀行賬戶。一個帳戶的余額是 以太 (在以太坊區塊鏈上使用的幣種),你可以和其他帳戶之間支付和接受以太幣,就像你的銀行帳戶可以電匯資金到其他銀行帳戶一樣。
每個帳戶都有一個“地址”,你可以把它想象成銀行賬號。這是賬戶唯一的標識符,它看起來長這樣:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
Mapping(映射)
//對于金融應用程序,將用戶的余額保存在一個 uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者可以用來通過userId 存儲/查找的用戶名
mapping (uint => string) userIdToName;
msg.sender
在 Solidity 中,有一些全局變量可以被所有函數調用。 其中一個就是 msg.sender,它指的是當前調用者(或智能合約)的 address。
注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什么也不做,除非有人調用其中的函數。所以 msg.sender 總是存在的。
注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什么也不做,除非有人調用其中的函數。所以 msg.sender 總是存在的。
以下是使用**** msg.sender 來更新 mapping 的例子:
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// 更新我們的 favoriteNumber
映射來將 _myNumber
存儲在 msg.sender
名下
favoriteNumber[msg.sender] = _myNumber;
// 存儲數據至映射的方法和將數據存儲在數組相似
}
function whatIsMyNumber() public view returns (uint) {
// 拿到存儲在調用者地址名下的值
// 若調用者還沒調用 setMyNumber,則值為 0
return favoriteNumber[msg.sender];
}
Require
function sayHiToVitalik(string _name) public returns (string) {
// 比較 _name 是否等于 "Vitalik". 如果不成立,拋出異常并終止程序
// (敲黑板: Solidity 并不支持原生的字符串比較, 我們只能通過比較
// 兩字符串的 keccak256 哈希值來進行判斷)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 運行如下語句
return "Hi!";
}
繼承 Inheritance
有個讓**** Solidity 的代碼易于管理的功能,就是合約 inheritance (繼承):
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
由于 BabyDoge 是從 Doge 那里 inherits (繼承)過來的。 這意味著當你編譯和部署了 BabyDoge,它將可以訪問 catchphrase() 和 anotherCatchphrase()和其他我們在 Doge 中定義的其他公共函數。
引入****Import
在 Solidity 中,當你有多個文件并且想把一個文件導入另一個文件時,可以使用 import 語句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
Storage與Memory
在 Solidity 中,有兩個地方可以存儲變量 —— storage 或 memory。
Storage 變量是指永久存儲在區塊鏈中的變量。 Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。 你可以把它想象成存儲在你電腦的硬盤或是RAM中數據的關系。
大多數時候你都用不到這些關鍵字,默認情況下 Solidity 會自動處理它們。 狀態變量(在函數之外聲明的變量)默認為“存儲”形式,并永久寫入區塊鏈;而在函數內部聲明的變量是“內存”型的,它們函數調用結束后消失。
internal 和 external
internal 和 private 類似,不過, 如果某個合約繼承自其父合約,這個合約即可以訪問父合約中定義的“內部”函數。(嘿,這聽起來正是我們想要的那樣!)。
external 與public 類似,只不過這些函數只能在合約之外調用 - 它們不能被合約內的其他函數調用。稍后我們將討論什么時候使用 external 和 public。
Ownable
下面是一個 Ownable 合約的例子: 來自 OpenZeppelin Solidity 庫的 Ownable 合約。 OpenZeppelin 是主打安保和社區審查的智能合約庫,您可以在自己的 DApps中引用。等把這一課學完,您不要催我們發布下一課,最好利用這個時間把 OpenZeppelin 的網站看看,保管您會學到很多東西!
- 構造函數:function Ownable()是一個 constructor (構造函數),構造函數不是必須的,它與合約同名,構造函數一生中唯一的一次執行,就是在合約最初被創建的時候。
- 函數修飾符:modifier onlyOwner()。 修飾符跟函數很類似,不過是用來修飾其他已有函數用的, 在其他語句執行前,為它檢查下先驗條件。 在這個例子中,我們就可以寫個修飾符 onlyOwner 檢查下調用者,確保只有合約的主人才能運行本函數。我們下一章中會詳細講述修飾符,以及那個奇怪的_;。
- indexed 關鍵字:別擔心,我們還用不到它。
- @title Ownable*
- @dev The Ownable contract has an owner address, and provides basic authorization control*
- functions, this simplifies the implementation of "user permissions".*
/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
** @dev The Ownable constructor sets the original owner
of the contract to the sender*
** account.*
*/
function Ownable() public {
owner = msg.sender;
}
/**
** @dev Throws if called by any account other than the owner.*
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
** @dev Allows the current owner to transfer control of the contract to a newOwner.*
** @param newOwner The address to transfer ownership to.*
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
Gas-驅動以太坊DApps的能源
在 Solidity 中,你的用戶想要每次執行你的 DApp 都需要支付一定的 gas,gas 可以用以太幣購買,因此,用戶每次跑 DApp 都得花費以太幣。
一個 DApp 收取多少 gas 取決于功能邏輯的復雜程度。每個操作背后,都在計算完成這個操作所需要的計算資源,(比如,存儲數據就比做個加法運算貴得多), 一次操作所需要花費的 gas 等于這個操作背后的所有運算花銷的總和。
由于運行你的程序需要花費用戶的真金白銀,在以太坊中代碼的編程語言,比其他任何編程語言都更強調優化。同樣的功能,使用笨拙的代碼開發的程序,比起經過精巧優化的代碼來,運行花費更高,這顯然會給成千上萬的用戶帶來大量不必要的開銷。
為何要****gas來驅動?
以太坊就像一個巨大、緩慢、但非常安全的電腦。當你運行一個程序的時候,網絡上的每一個節點都在進行相同的運算,以驗證它的輸出 —— 這就是所謂的”去中心化“ 由于數以千計的節點同時在驗證著每個功能的運行,這可以確保它的數據不會被被監控,或者被刻意修改。
可能會有用戶用無限循環堵塞網絡,抑或用密集運算來占用大量的網絡資源,為了防止這種事情的發生,以太坊的創建者為以太坊上的資源制定了價格,想要在以太坊上運算或者存儲,你需要先付費。
注意:如果你使用側鏈,倒是不一定需要付費,比如咱們在 Loom Network 上構建的 CryptoZombies 就免費。你不會想要在以太坊主網上玩兒“魔獸世界”吧? - 所需要的 gas 可能會買到你破產。但是你可以找個算法理念不同的側鏈來玩它。我們將在以后的課程中咱們會討論到,什么樣的 DApp 應該部署在太坊主鏈上,什么又最好放在側鏈。
時間單位
readyTime 稍微復雜點。我們希望增加一個“冷卻周期”,表示僵尸在兩次獵食或攻擊之之間必須等待的時間。如果沒有它,僵尸每天可能會攻擊和繁殖1,000次,這樣游戲就太簡單了。
Solidity 使用自己的本地時間單位。
變量 now 將返回當前的unix時間戳(自1970年1月1日以來經過的秒數)。我寫這句話時 unix 時間是 1515527488。
注意:Unix時間傳統用一個32位的整數進行存儲。這會導致“2038年”問題,當這個32位的unix時間戳不夠用,產生溢出,使用這個時間的遺留系統就麻煩了。所以,如果我們想讓我們的 DApp 跑夠20年,我們可以使用64位整數表示時間,但為此我們的用戶又得支付更多的 gas。真是個兩難的設計啊!
Solidity 還包含秒(seconds),分鐘(minutes),小時(hours),天(days),周(weeks) 和 年(years) 等時間單位。它們都會轉換成對應的秒數放入 uint 中。所以 1分鐘 就是 60,1小時是 3600(60秒×60分鐘),1天是86400(24小時×60分鐘×60秒),以此類推
函數修飾符
- 1、我們有決定函數何時和被誰調用的可見性修飾符: private 意味著它只能被合約內部調用; internal 就像 private 但是也能被繼承的合約調用; external 只能從合約外部調用;最后 public 可以在任何地方調用,不管是內部還是外部。
- 2、我們也有狀態修飾符, 告訴我們函數如何和區塊鏈交互: view 告訴我們運行這個函數不會更改和保存任何數據; pure 告訴我們這個函數不但不會往區塊鏈寫數據,它甚至不從區塊鏈讀取數據。這兩種在被從合約外部調用的時候都不花費任何gas(但是它們在被內部其他函數調用的時候將會耗費gas)。
- 3、然后我們有了自定義的 modifiers,例如在第三課學習的: onlyOwner 和 aboveLevel。 對于這些修飾符我們可以自定義其對函數的約束邏輯。
payable修飾符
payable 方法是讓 Solidity 和以太坊變得如此酷的一部分 —— 它們是一種可以接收以太的特殊函數。
先放一下。當你在調用一個普通網站服務器上的API函數的時候,你無法用你的函數傳送美元——你也不能傳送比特幣。
但是在以太坊中, 因為錢 (以太), 數據 (事務負載), 以及合約代碼本身都存在于以太坊。你可以在同時調用函數 并付錢給另外一個合約。
這就允許出現很多有趣的邏輯, 比如向一個合約要求支付一定的錢來運行一個函數。
示例
contract OnlineStore {
function buySomething() external payable {
// 檢查以確定0.001以太發送出去來運行函數:
require(msg.value == 0.001 ether);
// 如果為真,一些用來向函數調用者發送數字內容的邏輯
transferThing(msg.sender);
}
}
提現
在上一節,我們學習了如何向合約發送以太,那么在發送之后會發生什么呢?
在你發送以太之后,它將被存儲進以合約的以太坊賬戶中, 并凍結在哪里 —— 除非你添加一個函數來從合約中把以太提現。
你可以寫一個函數來從合約中提現以太,類似這樣:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
隨機數
優秀的游戲都需要一些隨機元素,那么我們在 Solidity 里如何生成隨機數呢?
真正的答案是你不能,或者最起碼,你無法安全地做到這一點。
我們來看看為什么
用** keccak256 **來制造隨機數
Solidity 中最好的隨機數生成器是 keccak256 哈希函數.
我們可以這樣來生成一些隨機數
// 生成一個0到100的隨機數:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
這個方法首先拿到 now 的時間戳、 msg.sender、 以及一個自增數 nonce (一個僅會被使用一次的數,這樣我們就不會對相同的輸入值調用一次以上哈希函數了)。
然后利用 keccak 把輸入的值轉變為一個哈希值, 再將哈希值轉換為 uint, 然后利用 % 100 來取最后兩位, 就生成了一個0到100之間隨機數了。
這個方法很容易被不誠實的節點攻擊
在以太坊上, 當你在和一個合約上調用函數的時候, 你會把它廣播給一個節點或者在網絡上的 transaction 節點們。 網絡上的節點將收集很多事務, 試著成為第一個解決計算密集型數學問題的人,作為“工作證明”,然后將“工作證明”(Proof of Work, PoW)和事務一起作為一個 block 發布在網絡上。
一旦一個節點解決了一個PoW, 其他節點就會停止嘗試解決這個 PoW, 并驗證其他節點的事務列表是有效的,然后接受這個節點轉而嘗試解決下一個節點。
這就讓我們的隨機數函數變得可利用了
我們假設我們有一個硬幣翻轉合約——正面你贏雙倍錢,反面你輸掉所有的錢。假如它使用上面的方法來決定是正面還是反面 (random >= 50 算正面, random < 50 算反面)。
如果我正運行一個節點,我可以 只對我自己的節點 發布一個事務,且不分享它。 我可以運行硬幣翻轉方法來偷窺我的輸贏 — 如果我輸了,我就不把這個事務包含進我要解決的下一個區塊中去。我可以一直運行這個方法,直到我贏得了硬幣翻轉并解決了下一個區塊,然后獲利。
所以我們該如何在以太坊上安全地生成隨機數呢?
因為區塊鏈的全部內容對所有參與者來說是透明的, 這就讓這個問題變得很難,它的解決方法不在本課程討論范圍,你可以閱讀 這個 StackOverflow 上的討論 來獲得一些主意。 一個方法是利用 oracle 來訪問以太坊區塊鏈之外的隨機數函數。
ERC20 ERC721標準(七)
預防溢出
僵尸轉移給0 地址(這被稱作 “燒幣”, 基本上就是把代幣轉移到一個誰也沒有私鑰的地址,讓這個代幣永遠也無法恢復)
假設我們有一個 uint8, 只能存儲8 bit數據。這意味著我們能存儲的最大數字就是二進制 11111111 (或者說十進制的 2^8 - 1 = 255).
來看看下面的代碼。最后 number 將會是什么值?
uint8 number = 255;
number++;
在這個例子中,我們導致了溢出 — 雖然我們加了1, 但是 number 出乎意料地等于 0了。 (如果你給二進制 11111111 加1, 它將被重置為 00000000,就像鐘表從 23:59 走向 00:00)。
下溢(underflow)也類似,如果你從一個等于 0 的 uint8 減去 1, 它將變成 255 (因為 uint 是無符號的,其不能等于負數)。
使用 SafeMath
為了防止這些情況,OpenZeppelin 建立了一個叫做 SafeMath 的 庫(library),默認情況下可以防止這些問題。
不過在我們使用之前…… 什么叫做庫?
一個庫是 Solidity 中一種特殊的合約。其中一個有用的功能是給原始數據類型增加一些方法。
比如,使用 SafeMath 庫的時候,我們將使用 using SafeMath for uint256 這樣的語法。 SafeMath 庫有四個方法 — add, sub, mul, 以及 div。現在我們可以這樣來讓 uint256 調用這些方法:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
assert和require區別
assert 和 require 相似,若結果為否它就會拋出錯誤。 assert 和 require 區別在于,require 若失敗則會返還給用戶剩下的 gas, assert 則不會。所以大部分情況下,你寫代碼的時候會比較喜歡 require,assert 只在代碼可能出現嚴重錯誤的時候使用,比如 uint 溢出。
Solidity 社區所使用的一個標準是使用一種被稱作 natspec 的格式,看起來像這樣:
/// @title 一個簡單的基礎運算合約
/// @author H4XF13LD MORRIS
/// @notice ???ú£??a??o?????ìí?óò???3?·¨