從Python角度了解以太坊
區塊鏈基礎
以太坊(Ethereum)的底層是區塊鏈技術,而區塊鏈簡單而言就是由Hash值串聯起來的鏈表結構,鏈表中的節點會記錄交易信息,如果將節點中的信息以JSON格式描述,看起來是這個樣子:
- {
- "number": 1234567,
- "hash": "0xabc123...",
- "parentHash": "0xdef456...",
- "miner": "0xa1b2c3...",
- ...,
- "transactions": [...]
- }
- hash:當前節點的Hash值
- parentHash:前一個節點的Hash值
- miner:礦工地址
- transactions:當前節點包含的交易數據
礦工接收交易數據后,會將其封裝到一個區塊里,并將區塊信息廣播到以太坊網絡中,這里會有很多細節,建議找本書看看,這里想強調的是,想讓礦工干活,需要花錢,在以太坊上,其貨幣稱為「ether」。
使用Web3.py
Web3.py是一個用于與以太坊網絡交互的Python庫,它封裝了很多操作,便于我們進行交易、與智能合約交互、讀取區塊中的數據等等。
Web3.py官方文檔:https://web3py.readthedocs.io/en/stable/index.html
通過一張圖,可以很清晰的知道我們開發的應用、Web3.py以及是以太坊網絡的關系。
從上圖可知,Web3.py其實就是中間層,它可以通過HTTP、IPC(進程間通信)、WebSocket的方法連接到以太坊節點,從而實現與整個以太坊網絡的交互。
使用前,我們需要安裝Web3.py:
- pip install web3
- pip install 'web3[tester]'
安裝web3[tester]的目的時,使用Web3.py提供的模擬節點進行測試,如果我們要同步真正的節點需要做:
- 1.下載Geth構建以太坊節點。
- 2.啟動Geth并等待它同步以太坊網絡中的數據,Geth默認會啟動HTTP服務,端口為8545.
- 3.使用Web3.py通過HTTP連接到剛剛構建好的節點。
- 4.使用Web3.py提供的API與節點進行交互
Geth是使用Go實現Ethereum協議的程序,與之類似的還是使用C++或Python實現的,只是Geth勢頭第一。
同步過程需要拉取數據,可能需要幾個小時。
這里只是演示,所以直接使用模擬節點就好了,如下圖:
上圖中,Web3.py提供了4種接入以太坊節點的方式,其中第4種便是通過TesterProvider接入模擬的以太坊節點,要使用這個功能,你需要安裝web3[tester]。
安裝好web3后,先通過TesterProvider方法連接到模擬節點中。
- In [1]: from web3 import Web3
- # 使用EthereumTesterProvider,連接模擬節點
- In [2]: w3 = Web3(Web3.EthereumTesterProvider())
- # 判斷連接是否正常
- In [3]: w3.isConnected()
- Out[3]: True
為了方便我們測試(如測試編寫好的智能合約),Web3.py提供的模擬節點中已經為我們提供了一些賬戶,每個賬戶中有1000000個ether。
- # 獲得可以使用的測試賬戶
- In [4]: w3.eth.accounts
- Out[4]:
- ['0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf',
- '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF',
- '0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69',
- '0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718',
- '0xe1AB8145F7E55DC933d51a18c793F901A3A0b276',
- '0xE57bFE9F44b819898F47BF37E5AF72a0783e1141',
- '0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb',
- '0xF1F6619B38A98d6De0800F1DefC0a6399eB6d30C',
- '0xF7Edc8FA1eCc32967F827C9043FcAe6ba73afA5c',
- '0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528']
- # 獲得第一個賬戶下的金額
- In [5]: w3.eth.get_balance(w3.eth.accounts[0])
- Out[5]: 1000000000000000000000000
使用w3.eth.get_balance方法獲得的金額為1000000000000000000000000,是因為其單位是wei。
因為計算機不太擅長處理浮點數,所以為了解決這個問題,很多程序員會將1.23元以123存到數據庫中,即以分為基本單位。以太坊這邊也一樣,ether類似于元的單位,而wei類似于分,只是分只是增大了100倍,而wei與ether的比例是(18個0):
- 1 ether = 100000000000000000 wei
- 1 wei = 0.0000000000000000001 ether
Web3.py提供了toWei與fromWei方法進行單位的換算,ether與wei單位之間還有多個單位,可以查閱Web3.py文檔Converting currency denominations。簡單使用toWei與fromWei兩個方法:
- In [7]: Web3.toWei(1, 'ether')
- Out[7]: 1000000000000000000
- In [8]: Web3.fromWei(1000000000000000000000000, 'ether')
- Out[8]: Decimal('1000000')
模擬交易
有了賬戶以及錢后,就可以模擬交易行為了,即將你賬戶中的幣轉到其他賬戶中。
先來看看,沒有任何轉賬狀態下的區塊鏈:
- # 獲取區塊鏈中最新一個區塊的信息
- In [9]: w3.eth.get_block('latest')
- Out[9]:
- AttributeDict({'number': 0,
- 'hash': HexBytes('0x78b6514d115669937c0933824a0c74ff2eab14a25f1b1e799609872bcb18113b'),
- # 前一個區塊Hash為0
- 'parentHash': HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
- ...
- 'gasLimit': 3141592,
- 'gasUsed': 0,
- 'timestamp': 1635092566,
- # 沒有交易
- 'transactions': [],
- 'uncles': []})
因為是模擬節點,所以與真實節點不同,它不會在大約15秒內增加一個新區塊,而是會一直模擬等待,直到你進行交易。
到目前為止,因為我們沒有進行任何交易,所以parentHash(前置區塊Hash)為0,transactions(交易數據)為空,這個區塊,其實就是創世區塊。
現在我們進行一筆交易,如下:
- # 發起一筆交易
- In [10]: tx_hash = w3.eth.send_transaction({
- ...: 'from': w3.eth.accounts[0],
- ...: 'to': w3.eth.accounts[1],
- ...: 'value': w3.toWei(3, 'ether')
- ...: })
- from:發送者賬戶的地址
- to:接受者賬戶的地址
- value:此次轉賬金額
我們可以通過get_transaction獲得這次交易更詳細的信息,如下:
- # 獲取那筆交易的信息
- In [14]: w3.eth.get_transaction(tx_hash)
- Out[14]:
- AttributeDict({'hash': HexBytes('0x15e9fb95dc39da2d70f4cc41556bd092c68a97a04892426a064e321bfe78662a'),
- 'nonce': 0,
- 'blockHash': HexBytes('0x9f92558e214519a5e4ba7b8b4769a59bdc8c6c13e6fe5b0ec062b806e18f049f'),
- # 交易數據在第一個區塊中
- 'blockNumber': 1,
- # 全網絡第一個交易
- 'transactionIndex': 0,
- 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf',
- 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF',
- 'value': 3000000000000000000,
- 'gas': 21000,
- 'gasPrice': 1,
- 'data': '0x',
- 'v': 28,
- 'r': HexBytes('0x11bebd35f91582f55dc180dcfc1c5ccad48dadc207802727f7ac997df6490b22'),
- 's': HexBytes('0x697db707f5b7cc4d3a3196b434b0d5616300b8afbe8a21ab47ed9335252e4ebd')})
完成交易后,我們可以查詢對應賬戶的余額來判斷是否轉賬成功。
- In [15]: w3.eth.get_balance(w3.eth.accounts[0])
- Out[15]: 999996999999999999979000
- In [16]: w3.eth.get_balance(w3.eth.accounts[1])
- Out[16]: 1000003000000000000000000
可以看到,第二個賬戶,余額從1,000,000到1,000,003 ether,但第一個賬戶減少的金額卻超過3 ether,這是因為交易需要扣除一筆小額手續費所導致的。
在真實的以太坊網絡中,交易手續費用是可以調整的,這取決于你發起交易時的網絡需求以及你希望處理交易的速度,就目前而言,交易手續費是比較大的成本。
創建賬戶
在Web 2.0中,大多數應用在使用前需要創建賬戶,這個賬戶會保存到公司服務器中,即你雖然使用賬戶,但卻沒有賬戶的所有權,以微信為例,如果哪天騰訊公司要封掉你的賬戶是非常輕松的,這個賬戶下對你非常重要的數據將與你不辭而別。
在Web 3.0中,你同樣可以創建賬戶,創建完后,你會擁有該賬戶的私鑰與公鑰,這個賬戶不會受到應用創建者或公司的影響,只要你不泄露私鑰,那么賬戶的所有權就在你手中,當然也要一些隱患,如果你丟失了私鑰,那么就丟失了賬戶,沒有找回密碼一說,所以在幣圈,非常強調對自己私鑰的保護。
通過Web3.py,我們可以輕松的創建一個賬戶,這個過程是無需連接到區塊鏈網絡或任何服務的,也沒有注冊過程,如下:
- # 創建賬戶
- In [17]: acct = w3.eth.account.create()
- # 賬戶地址
- In [19]: acct.address
- Out[19]: '0x004D8ae69CD02Be5c491F7D095b5585cECE01407'
- # 賬戶私鑰(不可泄露給他人)
- In [20]: acct.key
- Out[20]: HexBytes('0x39a579d1302e36fbf8b283eca5e1d52b4c56811921dfcdc2996f59eff7be6258')
再次強調,你不需要聯網,不需要提供任何其他信息,便可以創建一個有效的以太坊賬戶,后續我會寫一下賬戶生成的過程。
賬戶是一個重要的概念,因為我們影響區塊鏈產生變化的唯一方式便是產生一個交易(調用智能合約也看為產生一個交易),而每個交易必須由一個賬戶進行簽名,避免別人偽冒。
一個賬戶可以進行交易、進行交易間信息的傳輸、可以部署智能合約、可以與智能合約進行交互等。
首先,我們實踐一下,如何通過賬戶進行轉賬。
回顧前面的代碼,我們通過EthereumTesterProvider連接的模擬節點并有一些賬戶,這些賬戶的轉賬過程通過send_transaction方法完成,這里隱藏了比較多細節,因為Web3.py知道你在使用EthereumTesterProvider管理的測試賬戶,而這些賬戶都處在unlocked狀態,即默認情況下,這些賬戶的交易都會自動完成簽名。
這次,我們從自己創建的賬戶轉賬看看,因為我們自己的賬戶沒有ether,所以需要先從測試賬戶轉點錢過去。
- In [25]: w3.eth.get_balance(acct.address)
- Out[25]: 0
- In [26]: w3.eth.get_balance(test_acct)
- Out[26]: 1000000000000000000000000
- # 測試賬戶轉10000000000到創建的賬戶中
- In [27]: tx_hash = w3.eth.send_transaction({
- ...: 'from': test_acct,
- ...: 'to': acct.address,
- ...: 'value': 10000000000
- ...: })
- In [28]: tx_hash
- Out[28]: HexBytes('0xa1b8be56bee0421035cbb9afb157218770f692c71b553a82cb52529c5dd12c3d')
創建的賬戶中有錢了,現在使用創建的賬戶來完成一筆交易,這個過程,我們需要手動對交易數據進行簽名。
- # 交易數據
- In [29]: tx_data = {
- ...: 'to': test_acct,
- ...: 'value': 500000000,
- ...: 'gas': 21000,
- ...: 'gasPrice': 1, # 這個gas價格只存在于測試網絡中
- ...: 'nonce': 0
- ...: }
- # 使用acct的私鑰對交易數據進行簽名
- In [30]: signed = w3.eth.account.sign_transaction(tx_data, acct.key)
- In [31]: signed
- Out[31]: SignedTransaction(rawTransaction=HexBytes('0xf8638001825208946813eb9362372eef6200f3b1dbc3f819671cba69841dcd6500801ca029f2b216949529fbd19841c52e0cc78f218f45dd3531b918224f345f2e381aa9a0266619842d80050ab00bf8e0ea383056d7691a955113764e86927ddf36a478bb'), hash=HexBytes('0x402a89616ea2c37af4a17d8ff527e83141ac39968f3a61ddf92d4a1f5830cd29'), r=18973632901206428005591964593075310485747666570252293259781563419879236180649, s=17368282753934865160480197991111055873845272890414273881219608275127669913787, v=28)
- # 進行交易
- In [32]: tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
- In [33]: tx_hash
- Out[33]: HexBytes('0x402a89616ea2c37af4a17d8ff527e83141ac39968f3a61ddf92d4a1f5830c
先關注到交易數據,其中gas、gasPrice、nonce是比較細的東西。
gas:表示這次交易需要花費多少gas(燃氣)
gasPrice:表示gas的價格,在真實的以太坊網絡中,gasPrice是很高的,也是目前以太坊網絡的一個問題與痛點
none:在以太坊網絡中,none表示當前賬戶的交易數,Ethereum protocol(以太坊協議)會追蹤這個值,避免發生雙重支付(也稱雙花攻擊),這里為0表示acct賬戶第一次產生交易。
需要注意,無論是send_transaction方法還是send_raw_transaction方法,都不代表交易完成了,這些方法只是將交易數據廣播到以太坊網絡中,只有當以太坊網絡中的礦工節點將交易上鏈了,才能說交易完成了,如果你的gas與gasPrice很小,在真實以太坊網絡中,你將很難上鏈,即難以完成真正的交易。
從開發者角度看,以太坊上的這種賬戶模式可以讓我們在創建應用時,不太需要考慮賬戶管理等功能,以太坊已經天然解決了這部分問題,你可以將精力集中在具體的業務中。
與智能合約交互
以太坊被提出的一個重要原因是,比特幣網絡不支持圖靈完備的編程語言,導致很多應用無法被開發,以太坊則支持圖靈完畢的編程語言,如Solidity語言,它的語法與JS相近,是圖靈完備的語言,基于Solidity語言,可以快速開發智能合約。
簡單而言,智能合約就是存儲在以太坊區塊鏈上的程序,任何人都可以使用它,如果你需要部署一個智能合約,其過程與發起一筆交易類似,只是交易數據里,包含著Solidity編譯后的字節碼,偽代碼如下:
- # 代碼編譯成字節碼
- bytecode = "6080604052348015610...36f6c63430006010033"
- tx = {
- # 將字節碼作為數據包含在交易數據體中
- 'data': bytecode,
- 'value': 0,
- 'gas': 1500000,
- 'gasPrice': 1,
- 'nonce': 0
- }
部署智能合約相比于普通交易通常需要更多的gas,且部署智能合約的交易體中沒有to字段。
Web3.py將部署以及與智能合約交互的過程進行了簡化,偽代碼如下:
- # 部署一個新的智能合約
- Example = w3.eth.contract(abi=abi, bytecode=bytecode)
- tx_hash = Example.constructor().transact()
- # 通過智能合約的地址連接一個智能合約
- myContract = web3.eth.contract(address=address, abi=abi)
- # 傳參、使用智能合約
- twentyone = myContract.functions.multiply7(3).call()
生成簽名信息
賬戶除了可以進行交易等鏈上(on-chain)操作,還可以進行消息簽名等鏈下(off-chain)操作。
與交易不同,被簽名消息不需要上鏈,也不會被廣播到區塊鏈網絡中,即不需要花費任何成本,簡單而言,簽名消息只是用你的私鑰對數據進行了一個數學操作,當你將這段數據發送給他人時,他人可以通過數據方法還原出簽名私鑰對應的公鑰,從而確定這個數據是由你簽名的。
這有什么用?可以使用到NFT上。
你可以對你的作品(一段數據)進行簽名,然后到OpenSea(目前最大的NFT交易市場)進行售賣,當有賣家購買時,才會將購買時產生的交易數據上鏈,上鏈的過程需要花費ether,而你簽名的過程是不需要任何成本的,上鏈的操作其實只是表明你簽名的這個作品被某個賬戶購買了,這個購買的交易操作產生的數據會記錄到區塊鏈中,是不可更改的。
通過一段偽代碼,可以更直觀的理解簽名消息的整個流程:
- # 1. 待簽名數據
- msg = "我是二兩,給我打錢"
- # 2. 使用你賬戶的私鑰進行前面
- pk = b"..."
- signed_message = sign_message(message=msg, private_key=pk)
- # 3. 通過網絡發送簽名后的數據
- # 4. 消息接收者解碼發送的數據,獲得數據的公鑰,從而可以確定發送消息者的身份
- sender = decode_message_sender(msg, signed_message.signature)
- print(sender)
參考
A Developer's Guide to Ethereum, Pt. 1
A Developer's Guide to Ethereum, Pt. 2