freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

区块链学习笔记之初探以太坊
2022-02-22 13:46:11
所属地 广东省

属于是一个密码手不好好学密码,又来以太坊开新坑了。主要是之前在先知上看到对一道智能合约练习题目的解析,里面用到了ECDSA相关的临时密钥重用攻击,从而恢复了合约owner的私钥。虽然在读了签名的生成代码(python的web3模块)后意识到了在实际运用中几乎是碰不见这样的场景,但由此也是对智能合约产生了那么一丢丢兴趣,从而开始走上了又一条不归路

在写下这篇文章前已经是学了些以太坊相关的杂七杂八的知识,所以也是想通过这么一篇文章来对之前习得的知识做一个梳理、总结,也希望给初学者们提供一点“前车之鉴”。但由于是初学,也还没有很深入的去看以太坊的底层源码,所以对以太坊的一些东西应该还是会存在一些想当然的误解,希望在学习的过程中能不断的勘误,当然有大佬指正则是更好了。

区块链这么个大概念这里就不重复的叙述了,相关的资料和书籍也都有很多,我们直接切入主题。首先我们需要知道的是,作为一个去中心化的东西,没了中心化,那谁提供服务呢?是处于世界各地的节点,P2P的模式,就是每一个人都是服务的享受着,也是服务的提供者。我们在接受信息的同时也在传递并维护(感觉提供服务,维护信息主要还是矿工做的事)着信息。每个人都可以作为一个节点,但这个节点呢,也有区别,它可以在不同的链中。我们知道除了主链以外呢,还会有很多测试链,是的呀,项目落地前总的先测试一下叭,本地测试可以搭私链,想让更多的人访问的话也可以选择那些公认的测试链。在私链上你可以给自己账户生成很多测试代币,测试链上你可以找相应的水管(faucets),水管是啥?就是无偿获取测试币的地方。

如何领取测试币,以ropsten和rinkeby为例

获取测试币

rinkeby

原来的水管似乎有点问题,可以去这里https://faucets.chain.link/rinkeby,输入你的账户(可以利用MetaMask插件生成一个账户)地址,一次可以获取0.1eth

image-20220214111434710

image-20220214111403997

ropsten

https://faucet.ropsten.be/这里输入你的账户地址,一次会给【0.3】eth,不过要等比较久似乎。

还有一个就是MetaMask的水管https://faucet.metamask.io/

image-20220214140125865

上面是获取 1 eth,下面是给他捐 eth(如果你账户上有很多 eth 的话就没法再获取了)

前面提到了两个链,我们可以把这两个链理解成两个平行世界,在这个世界里的交易和那个世界里是毫无关系的,但这两个时间中与合约相关的规则都是一样的,比如部署的合约地址运算规则,部署合约消耗的gas之类的。并且一个账户也是可以在所有链上存在的。

RPC

那么我们如何连接上这些链呢?我这用的是RPC,可以理解为链上的一个节点,你通过这个节点作为媒介和链上的其他账户进行交互。那么如何获取一个RPC呢,可以选择去infura.io注册一个账户,创建一个项目,它会提供相应的代理RPC【比如这里就是ropsten的】image-20220214143229332

然后你在程序里【以python为例】输入w3 = Web3(HTTPProvider("https://ropsten.infura.io/v3/*************"))相当于就是连上这条测试链了。不过你还得在配置里允许一些在进行交易的时候用的方法,不然就都会被ban

image-20220214143540312

另一个比较方便的是用MetaMask

MetaMask

用MetaMask钱包插件去管理账户还是挺方便的。之所以称之为“钱包”,是因为在里面你不仅可以保存多个账户,也可以根据场景随意的进行主链、测试链的切换。具体安装和注册这里就不介绍了,google商店走一波就好。弄好了之后,在上面注册或者导入一个账户,然后我们点击头像->设置->网络,选择你想看的链,就能看到MetaMask用的RPC了(其实也是infura的)

image-20220214144122140

合约

现在呢我们有自己的账户了,那么该怎么去和其他外部账户和合约账户交互呢?和外部账户的交互就只有转账了,这个可以用MetaMask,至于和合约账户的交互,这里比较方便的就是用remix,有在线的,也有离线的(我比较喜欢用在线的)

remix

首先得有一个合约,solidity相关的就不在这里涉及了。然后编译(建议翻墙,不然好像没编译器用)

image-20220214152039716

;然后就可以选择部署

image-20220214152420679

1.部署的环境有三种

image-20220214152454547

前两个都是JVM,纯纯本地测试用了

第三个就是和你的MetaMask交互,用测试链

第四个就是你自己提供的RPC,就是私链(这个我还没用过,好像在RPC那里另外设置一下)

2.部署账户,如果是JVM的话,他会给账户,而且有足够的钱,如果是测试链,他会获取你MetaMask用的账户,第三种没用过,,还不知道

3.设置一个部署合约会花的gas的上限

4.往部署的合约里塞点初始的资金(可选)

5.选择你编译好的要部署的合约

6.有些合约部署的时候需要参数,这根据你合约的构造函数来的

7.如果合约已经部署好了(被你,或者是别人),可以直接填入那个合约部署的地址

部署好了之后就可以开始交互了

image-20220214153314709

合约的名字旁边就是合约的地址,然后下面是一个可以调用的方法。需要参数的它会给个框框出来。有些调用只是获取一些值,不涉及更改合约的状态,也就不需要gas和value,会直接返回值。而有些称之为交易的调用,是需要gas,(有的还要value)这个时候你的MetaMask会跳出来,帮你打包好交易,计算好要花的gas,设置一个合理的gasPrice,确定nonce,并签好名,最后让你点确定是否允许这次交易(毕竟是要花钱的事,还是得问问你的)。可以看到,MetaMask还是很好用的,不仅帮你存账户,和remix联动还可以帮你构造交易。但有时候会遇到私链,然后remix又用不上,这个时候,麻烦大了。

脚本交互

作为一个密码手,肯定还是比较用python去操作的。这里要用的是python的web3模块,pip安装走一波就好了。

部署合约

在部署合约之前,我们要先做一些准备工作,你得把合约给编译好(好像有python和solc-x联动的东西,但是我没用起来,所以我还是用的remix去编译的)然后拿到合约的字节码和ABI(接口),image-20220214155811685

接着决定在哪条链上部署这个合约,是作为一个业务在主链上运行呢?还是先部署到测试链上做一个测试,又或者是想部署到自己的私链上。不同的选择我们就需要用到不同的RPC,所以第一步,我们先得初始化,RPC作为一个参数,这里用的是私链

from web3 import Web3

w3 = Web3(HTTPProvider("http://114.115.157.63:8545/"))

然后部署合约是需要花钱的,花钱的话,我们就得需要一个账户

w3.eth.account.from_key('0x6160feb8eb4**********************')

可以先看看账户里的eth余额:

w3.eth.get_balance(account.address)

要是不多的话,就按照上述方法去搞点先

接着部署合约,需要字节码和abi

contract=w3.eth.contract(abi=abi,bytecode=opcode)
construct_txn = contract.constructor("hello").buildTransaction({
'from': account.address,
'nonce': w3.eth.getTransactionCount(account.address),
'gas': 5000000,
'gasPrice': w3.toWei('21', 'gwei')})
signed = account.signTransaction(construct_txn) # 用账户对交易签名  
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.waitForTransactionReceipt(tx_id)
address = receipt.contractAddress
contract_instance= w3.eth.contract(address=address, abi=abi)

下面是完整的创建合约的代码

from web3 import Web3
w3 = Web3() # 提前设置好了环境变量,给好RPC了
# w3 = Web3(HTTPProvider("http://114.115.157.63:8545/"))
account = w3.eth.account.from_key('0x6160feb8eb4*********************') //设定外部账户
abi=[
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "greet",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isSolved",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
opcode="608060405234...30008070033" #这玩意儿太长了,就不放全了
contract=w3.eth.contract(abi=abi,bytecode=opcode)
construct_txn = contract.constructor("hello").buildTransaction({
'from': account.address,
'nonce': w3.eth.getTransactionCount(account.address),
'gas': 5000000,
'gasPrice': w3.toWei('21', 'gwei')})
signed = account.signTransaction(construct_txn) # 用账户对交易签名  
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.waitForTransactionReceipt(tx_id)
address = receipt.contractAddress
contract_instance= w3.eth.contract(address=address, abi=abi)
>> construct_txn
{'value': 0,
'chainId': 3,
'from': '0x5bE571f66D2f98eFf8EB270475375f2D34d47B0B',
'nonce': 5,
'gas': 5000000,
'gasPrice': 21000000000,
'data': '0x608060405234...300080700330000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000',
'to': b''}

可以看到,部署合约的交易,里面的data是创建合约的opcode然后额外加了点东西,然后目标合约(to)是空的,

注意到部署的这个过程,首先我们输入了些参数构造了交易,包括交易发起者,nonce,gas 和 gasPrice。但看到construct_txn还返回了其他东西,是因为我们给了足够多的参数(opcode和abi),所以模块能帮我们构造好。如果我们拿不到abi,那么我们就得自己去构造整个完整的交易了(下面会讨论这种情况),

好了,交易构造完成了,为了证明这个交易确确实实是你发起的,你还需要为这个交易进行签名

signed = account.signTransaction(tx_hash)

之后我们就可以发起这笔合法交易了

tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)

返回的tx_id就是这笔交易的哈希,你可以在区块链浏览器上输出这个hash来查看这笔交易的状态,是成功了,还是失败了,也可以用 getTransactionReceipt , getTransaction 和 waitForTransactionReceipt 等函数进行查看

然后我们就可以合约进行交互了。

调用一些不需要花费gas的查询功能比较简单,比如调用合约的 greet 函数contract_instance.functions.greet().call()

如果是那些需要改变状态的函数【也称之为交易】,就需要币了,也就是钱。涉及到钱,那肯定就是要比较麻烦了。比如这里我先调用合约的 setGreeting 函数,首先我们要构造好这个交易,一个交易的内容包括,来源账户(from),目标账户(to),消耗的gas(gas),gas的价格(gasPrice),来源账户的交易序号(nonce),交易的内容【要执行的代码】(data),交易中夹带的金额(value)。

注意到几个特殊情况,

部署合约的交易的目标账户(to)是空的(0),

转账交易的数据(data)是空的,

下面是调用部署好的合约里的 setGreeting 函数的交易细节,可以看到其实和合约部署没有太大的区别,无非是不同函数的调用。然后data稍微不一样。(部署合约的data就是合约的opcode+调用构造函数的opcode,普通函数的调用就是调用函数的opcode)

tx_hash = contract_instance.functions.setGreeting("HelloChainFlag").buildTransaction({'gas': 1000000,
'gasPrice': w3.toWei('21', 'gwei'), 'from': account.address,
'nonce': w3.eth.getTransactionCount(account.address)}) # 构造交易
signed = account.signTransaction(tx_hash) # 用账户对交易签名  
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
print(w3.eth.getTransactionReceipt(tx_id.hex()))
print(w3.eth.getTransaction(tx_id.hex()))
=========================================================================================
AttributeDict({'blockHash': HexBytes('0x0fae6f7b46dd6ac9074183b1b1550b0fa857c7d406fb4b6ea03d3703b6942561'), 'blockNumber': 11681286, 'contractAddress': None, 'cumulativeGasUsed': 267017, 'effectiveGasPrice': 21000000000, 'from': '0x5bE571f66D2f98eFf8EB270475375f2D34d47B0B', 'gasUsed': 27050, 'logs': [], 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), 'status': 1, 'to': '0xbE7a7817Dd15eafeAD7917078D51C30Ca9E13fFf', 'transactionHash': HexBytes('0xcf8c3a5549aea014fe8ac5e5316b4670c91f9b97b43a02ef62414b77aa3c481b'), 'transactionIndex': 2, 'type': '0x0'})
AttributeDict({'blockHash': HexBytes('0x0fae6f7b46dd6ac9074183b1b1550b0fa857c7d406fb4b6ea03d3703b6942561'), 'blockNumber': 11681286, 'from': '0x5bE571f66D2f98eFf8EB270475375f2D34d47B0B', 'gas': 1000000, 'gasPrice': 21000000000, 'hash': HexBytes('0xcf8c3a5549aea014fe8ac5e5316b4670c91f9b97b43a02ef62414b77aa3c481b'), 'input': '0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e48656c6c6f436861696e466c6167000000000000000000000000000000000000', 'nonce': 5, 'r': HexBytes('0x5cb92f983c3c5d6cf2c1b66781514b9fa790ad135c522b051c841d2a7173cc2d'), 's': HexBytes('0x16abebfd0fa522f54a33f0fe691d81c92a5c7c6ea647534ff7ffd94611f7b4af'), 'to': '0xbE7a7817Dd15eafeAD7917078D51C30Ca9E13fFf', 'transactionIndex': 2, 'type': '0x0', 'v': 41, 'value': 0})

我们getTransactionReceipt 从中可以看到这笔交易所在的区块序号(blockNumber),区块哈希(blockHash),整个区块消耗的gas(cumulativeGasUsed),gas的价格(effectiveGasPrice),这个交易消耗的gas(gasUsed),交易的状态(status),交易的发起者(from),交易的接收者(to),交易哈希(transactionHash),交易日志(logs)【与触发事件有关】

getTransaction返回的值主要有这么几个不同,多了一个input,r,s,v。其中 r 和 s 是做ECDSA签名时用的值,是在验证签名的时候需要用到的参数,input记录了合约调用的信息,如果是0x,则说明是非合约调用,否则为合约调用,其实也就是我们的交易data。注意到,如果没有合约源码(合约不开源),那么我们就没法搞到abi文件,那么也就不能直接去调用合约的方法。这个时候就只能手动构造交易了。

需要填写以下参数

from : 自己合约的地址(好像不填就是默认的签名这个交易的account) nonce:这个可以通过w3.eth.getTransactionCount(account.address)获取 gas:这个自己设 gasPrice:这个也可以自己设,但是也可以用默认的w3.eth.gasPrice to:这个你要交易的合约地址,部署合约为空 value:此次交易要不要eth, data:交易的opcode,转账为空,其余情况下面单独讨论 chainId : 所在交易的链的id

opcode

可以看见,其他值的问题都不大,但是这个data,就需要了解一下以太坊的应用二进制接口,参考https://blog.csdn.net/JonasErosonAtsea/article/details/109236544

以合约方法function transfer(address to, uint tokens) 为例;

input数据分为3个部分:
4 字节,是方法名的哈希,例如:a9059cbb
32字节,放以太坊地址,目前以太坊地址是20个字节,高位补0
例如:000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca
32字节,是需要传输的代币数量,这里是1*10^18 GNT
例如:0000000000000000000000000000000000000000000000000de0b6b3a7640000
所有这些加在一起就是交易数据:
a9059cbb000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca0000000000000000000000000000000000000000000000000de0b6b3a7640000

回到这里,我们的input是

0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c68656c6c6f2056616e6973680000000000000000000000000000000000000000

前四字节是要调用的函数的哈希(以叫函数选择器) 0xa4136862

image-20211224174808777

然后32字节是 0x0000000000000000000000000000000000000000000000000000000000000020

这里先盲猜一下是从参数编码块起0x20的位置开始读内容

再32字节是0x000000000000000000000000000000000000000000000000000000000000000c

我们传进去字符串的长度 0x0c = 12

再32字节是0x68656c6c6f2056616e6973680000000000000000000000000000000000000000

我们传进去的字符串(末尾填充)

image-20211224175113352

更多情况参考https://learnblockchain.cn/docs/solidity/abi-spec.html

总结

这篇我们主要聊了一下,利用MetaMask创建管理以太坊(外部)账户,获取测试链上的测试代币,如何利用remix部署合约,联动MetaMask与合约进行交互,如何利用python的web3模块部署合约、进行交易,以及交易的一些细节,包括交易所需要的构造参数,交易的签名,以及交易的返回值,最后我们简单的聊了聊与合约调用相关的以太坊的应用二进制接口,也就是合约调用相关opcode的构造。

# 以太坊
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录