区块链基础
以太坊(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