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