Mar
14
以太坊区块链长什么样? —— 自建 ethereum 私有链指南
之前接触以太坊的时候,确实能搜到很多资料,还有一个看起来很丰富的 Homestead Documentation,但这些材料都太不接地气了,看完还是不知道以太坊区块链到底长什么样,因此整理了这篇说明,希望能够在一定程度上解决这个问题吧。
# 1. 安装 ethereum (@ubuntu)
参考 官方说明:
# 2. 生成创世文件
将以下内容保存于 ~/.ethereum/genesis.json
说明:
1) 来源于 https://g2ex.github.io/2017/09/12/ethereum-guidance/
2) 以太坊公链的chainID是1,私有链需要使用不同的chainID
3) 指定 ByzantiumBlock 这个参数,getTransactionReceipt() 才有 status ,用于判断交易执行结果
4) alloc 中指定了预分配 20个eth 的地址,其私钥是 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449
# 3. 初始化私有链
$ cd ~/.ethereum
$ geth --datadir ~/.ethereum/ init ./genesis.json
# 4. 账户基本操作
关于以太坊的私钥、公钥、地址、钱包的说明,参见第8节。
## 4.1 导入账户
$ echo 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449 > ~/.ethereum/coinbase.key
$ geth account import ~/.ethereum/coinbase.key
输入密码后,会在 ~/.ethereum/keystore/ 目录下生成一个账户文件,对应的地址就是创世文件里预分配了 20 eth 的那个 0xc1de867b55fdb749be0c927ecf7b19451777042b
## 4.2 生成新账户
执行以下命令,输入密码后会随机生成一个新的账户
$ geth account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {a90bbc1870e06a84b3dcf92a98f55453e6404a32}
## 4.3 列出账户
$ geth account list
Account #0: {c1de867b55fdb749be0c927ecf7b19451777042b} keystore:///data/www/.ethereum/keystore/UTC--2018-02-09T13-11-19.026113731Z--c1de867b55fdb749be0c927ecf7b19451777042b
Account #1: {7aa99ede746e863e87cdd1e686abadb31325cb92} keystore:///data/www/.ethereum/keystore/UTC--2018-02-18T09-00-05.982653587Z--7aa99ede746e863e87cdd1e686abadb31325cb92
# 5. 启动私有链
$ geth --identity "TestNode" --rpc --rpcport 8545 --port 30303 --nodiscover console
说明:
1) nodiscover 表示不会去连接其他节点(单节点私有链)
2) console 表示启动 geth 的 javascript 控制台
# 6. 控制台操作
在上一步启动的控制台里执行以下命令;或者通过 geth attach 连接到已经启动的 geth 实例执行。
## 6.1 查看账户
一般来说 coinbase 账户(挖矿的收益账户)是第一个导入的账户,也就是 eth.accounts[0]。
## 6.2 查看余额
## 6.3 转账
说明:
1) 以太币的最小单位 wei = 10^-18 ether
2) sendTransaction 的几个参数说明:
from: 交易发起方地址,必填,需要先解锁
to: 交易接收方地址,转账交易必填
value: 转账金额,单位为 wei
gasPrice: gas的价格,选填(geth会用估算值自动填充)
gas: 交易消耗的最大gas数量,选填(转账交易正好消耗21000,少于这个数字geth会拒绝创建交易); 多出的部分会在交易成功以后返还
data: 交易的输入数据,选填(转账交易不需要)
3) getTransaction 根据交易的hash获取交易详情,包括sendTransaction给出的参数,签名数据,和包含该交易的区块信息。由于当前还没有被确认,所以blockHash和blockNumber都是无效数据;特别提一下,nonce 其实也是 sendTransaction 的一个参数,相当于由该账户发起的交易的订单号,以太坊要求nonce严格有序递增(即下一笔订单的nonce一定是上一笔的nonce+1)。
关于gas和gasPrice,参见第9节。
由于交易还未被确认,所以暂不能得知交易的执行结果。
## 6.4 挖矿
## 6.5 查看交易执行结果
说明:
1) 交易已经被确认,所以 getTransaction 可以看到区块信息
2) getTransactionReceipt 得到交易回执,其中:
gasUsed: 实际消耗的gas(转账交易总是21000,除非指定了额外的data)
status: "0x1" 表示成功, "0x0" 表示失败
contractAddress: 新部署的合约地址;这个交易只是普通转账交易,不会部署合约,所以是null
logs: 记录发送给合约的交易触发的事件,普通转账交易不会产生事件,所以是 [] 。
# 7. 合约
以太坊的账户分成两种,一种是EOA账户(Externally Owned Account,由私钥控制),里面存储了账户的以太币余额信息和由该账户发起的交易数量(nonce),上面创建的都是EOA账户。另一种是合约账户(Contract Account),是由合约部署交易创建的,里面除了存储以太币余额,还存储了合约代码,以及代码的状态(存储)。通过创建并发送由某个EOA账户私钥签名的交易,可以从该账户给其他EOA账户转账,或者向某个合约账户发起一笔交易(调用合约账户里的某个方法);而合约账户只能被动触发,接受某个EOA账户(或其他被触发的合约账户)发起的交易,根据该交易的指令执行相应的动作,修改内部状态,或向其他账户发出交易。
所谓合约,并不是某个要被执行的合同,其实质是一段代码,可以类比为一个Class,其中定义了一些方法、状态、事件。将这段代码编译成以太坊虚拟机(Ethereum Virtual Machine,简称EVM)机器码,就可以作为 sendTransaction() 方法的 data 参数,被部署到以太坊区块链上(会自动分配一个地址),这样就创建了一个合约账户。后续按照EVM ABI约定发送到该合约账户的交易,可以指定触发这段代码的相应方法执行预定的操作。
这样的描述有点抽象,下面就用具体的栗子来说明合约账户。
## 7.1 ERC20
以太坊上线几年了,目前为止最成熟的应用就是发行 ERC20 Token ,圆了好多人的会所嫩模梦。所谓 ERC20 ,实际上就是一个 Interface ,凡是遵守这个 Interface 的合约,都可以被相同的方式触发。
具体的 ERC20 Interface 实现了多个方法和事件,详见Wiki:https://theethereum.wiki/w/index.php/ERC20_Token_Standard
下面的栗子里,只挑最重要的 balanceOf 和 transfer 这两个方法,以及 Transfer 这个事件:
从interface的定义可以比较直接的看懂,balanceOf用于查询某个地址的余额,tranfer用于转账到某个地址;如果转账成功,transfer方法会触发一个 Transfer 事件,记录在区块链上。
下面看一个具体的代码 (保存为 token.sol ):
## 7.2 编译合约
## 7.3 部署合约
从上面的编译结果 copy 字节码 和 abi json,执行下面的命令:
创建合约交易:初始化参数、合约部署方、gas limit;因为没有接收方,也不需要发送以太币,所以省略to和value
挖矿确认交易,然后看看部署的结果:
可以看到,发送的交易在被挖矿确认以后,部署到了 0x52ce1b10f214abb88517ceb3de2fbfbbfc305791 这个地址,消耗了 594886 个 gas 。这样就“发行”了一个ERC20 Token,名字是 Test Token,代码 TEST ,一共发行 10000 个,每个 token 精确到小数点后 2 位。
## 7.4 调用合约
上面的 contract 变量就是合约对象了,但不能每次想调用一个合约都得创建它,通常我们是需要直接调用某个合约地址里的方法,那就需要用这种方式创建合约对象:
余额查询:
可以看到,由于我们是用 coinbase 帐号发起的交易,合约在初始化的时候,根据合约代码的实现,把初始发行的 10000 个 TEST 都记到了 coinbase 帐号名下。要特别注意的是,这个账,不是记在 coinbase 这个EOA帐号的存储里,而是记在这个合约帐号的存储(代码里balances这个map)里面。
转账(误):
可以看到,虽然调用了transfer方法,但是 eth.accounts[1] 的余额并没变化。这是因为transfer方法是要修改存储的,而以太坊上的状态,只能由被区块链确认了的交易出发的动作修改。
那就试试往区块链上发送一笔转账交易:
挖矿确认这笔交易,然后查看执行结果:
向区块链发送一个交易,等到交易被确认以后,再查询 eth.accounts[1] 的 token 余额,就可以看到发生了变化。
这里有几个比较重要的信息:
1) getTransaction 返回结果里的 input
之前的普通转账交易,input的内容(就是sendTransaction里指定的data)都是 "0x" ,也就是 0 字节,无数据。而这个交易,有了一个比较复杂的值。这个值可以被拆成3段:
0xa9059cbb
=> 按照evm abi的约定,这是合约方法的签名的前4个字节。签名是用sha3对方法的定义进行hash计算出来的
=> 例如这个 0xa9059cbb 就来源于 sha3("transfer(address,uint256)")
00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd
=> 这就是接收方的地址,但用32字节表示,前面用0补齐
0000000000000000000000000000000000000000000000000000000000000064
=> 这是转账金额,32字节无符号整数,0x64 = 100
2) getTransactionReceipt 返回结果里的 logs
logs是个数组,其中的每一项代表一个被触发的 event ,这个栗子里,logs[0].topics 包含3个元素:
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
=> 这是事件的签名,sha3("Transfer(address,address,uint256)")
"0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b"
=> 这是发送方地址,也就是 eth.coinbase
"0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
=> 这是接受方地址,也就是 eth.coinbase
因为 Transfer 事件的定义里,from和to都带了一个 indexed 修饰符,所以会被记录到 topics 里,而没带 indexed 修饰符的 value ,则被记录在了 logs[0].data 里。
# 8. 以太坊的私钥、公钥、地址、钱包
以太坊和比特币一样,都是用的椭圆曲线加密算法 ECDSA-secp256k1。通常将私钥记为 k ,公钥记为 K 。下面使用node,安装bitcore-lib来演示。
## 8.1 私钥
私钥 k 通常是随机选出的 32 字节数据(可以看作一个 uint256 大整数),但要特别注意,不能使用伪随机数生成器。更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77,略小于2^256)。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。
## 8.2 公钥
将私钥 k 与一个指定的生成点 G 运算,就能得到公钥 K = k * G。K是椭圆曲线上的一个点 (x, y)。其中x和y都是uint256大整数。在以太坊里,公钥的二进制存储格式为 x + y,一共64字节。
## 8.3 地址
通过对公钥K做sha3(keccak256)哈希,得到一个32字节的结果,取其中的后20字节,就是以太坊的地址了:
这样就得到了一个以太坊的地址。为了检测在地址传递过程中发生的错误,EIP55给出了一个校验码方案,其思路很简单,对地址再做一次sha3,根据哈希结果修改地址中对应位置的字符大小写,详细方案参见:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md。
加上校验码后,地址长这样:0x2864Cdd53410820BC684F746871E9820fBb57243
## 8.4 钱包
由于以太坊和比特币都用的是 secp256k1 ,所以很多以太坊钱包是参考BIP32+BIP44实现的(包括KeepKey这样的硬件钱包),其中Coin Type用的是60H。
详情参考BIP44:https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
# 9. Gas/GasLimit/GasPrice
为了避免死循环导致的拒绝服务攻击,比特币区块链的目标是实现一个点对点的电子现金系统,因此其内置的脚本不是图灵完备的,不支持循环,只能完成一些条件判断。
以太坊有更大的野心,实现了一个图灵完备的EVM。为了解决这个拒绝服务攻击的问题,以太坊实现了这样一个机制:为了让EVM这台机器运转起来,你需要给它提供“加密燃料”——gas。gas是用以太币计价的,因此在发送交易的时候,需要指定购买gas的价格——gasPrice。由于停机问题不可解,为了避免过度消耗,交易发送者还需要指定gasLimit(对应sendTransaction方法的gas参数),表示他愿意为这笔交易的执行支付的最大gas数量。
当EVM收到开始执行一个交易的时候,会先冻结 value + gas * gasPrice 这些以太币(转账金额 + 为这笔交易愿意支付的最大金额),然后开始执行交易。以太坊的黄皮书里定义了不同操作需要消耗的gas数量,EVM在执行过程中,会根据定义好的规则累计消耗的gas总量。如果交易尚未执行完,但所有gas都消耗完,EVM会回滚所有改动,并将交易标记为失败(getTransactionReceipt返回的status='0x0')。无论交易最终是否成功,EVM累计消耗的gas都会被扣除,记录在 receipt 的 gasUsed 字段里,剩余的gas则按gasPrice换成以太币,退给交易发送者。特别地,普通转账交易消耗的gas数量正好是21000。作为以太坊网络的维护者和验证者,挖出包含这个交易的block的矿工可以获得这些被消耗的gas。
因此,如果有人要用死循环来恶意攻击以太坊网络,他就要付出无限的以太币。
# 10. 其他
## 10.1 以太币的总量
以太坊正式上线时,发行了72002454个以太币,每年会按这个数字的25%增发(即不超过1800万)。截止到目前(2018-03-17),流通总量为9823万(数据来源于 coinmarketcap.com)。
## 10.2 大整数:十六进制
由于以太币的最小单位 wei = 1/10^18 ether,所以连 UINT64 都无法保存所有的以太币。很多语言没有提供原生的大整数支持,所以以太坊的rpc接口传递大整数时都使用16进制编码,由语言自行处理。
## 10.3 版本
以太坊最早于是2015年5月上线 Olympic 测试网,7月30日上线 Frontier 版本。Frontier是beta版本,供开发者学习、体验,直到2016年3月14日(圆周率日)上线了正式的 Homestead 版本。
以太坊后续还规划了 Metropolis 和 Serenity 两个版本,上线时间尚未确定。
## 10.4 挖矿和共识机制
Homestead 的共识机制与比特币一样,都是PoW(Proof of Work),但使用了一个基于DAG的Ethash算法,对内存的要求较高,因此像ASIC矿机这种大规模嵌入运算单元的方式无法加速挖矿效率,从而尽量避免挖矿的中心化,增加网络的安全性。
在接下来的Metropolis版本里,以太坊计划切换为PoS机制,提高出块的效率。
## 10.5 还有啥?
想起来再补吧。
转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php 。
# 1. 安装 ethereum (@ubuntu)
参考 官方说明:
引用
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum solc
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum solc
# 2. 生成创世文件
将以下内容保存于 ~/.ethereum/genesis.json
引用
{
"config": {
"chainID": 1048576,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0,
"ByzantiumBlock": 0
},
"alloc": { "0xc1de867b55fdb749be0c927ecf7b19451777042b" : { "balance" : "20000000000000000000"} },
"coinbase": "0x0000000000000000000000000000000000000000",
"difficulty": "0x0400",
"extraData": "0x00",
"gasLimit": "0x2fefd8",
"nonce": "0xdeadbeefdeadbeef",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00"
}
"config": {
"chainID": 1048576,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0,
"ByzantiumBlock": 0
},
"alloc": { "0xc1de867b55fdb749be0c927ecf7b19451777042b" : { "balance" : "20000000000000000000"} },
"coinbase": "0x0000000000000000000000000000000000000000",
"difficulty": "0x0400",
"extraData": "0x00",
"gasLimit": "0x2fefd8",
"nonce": "0xdeadbeefdeadbeef",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00"
}
说明:
1) 来源于 https://g2ex.github.io/2017/09/12/ethereum-guidance/
2) 以太坊公链的chainID是1,私有链需要使用不同的chainID
3) 指定 ByzantiumBlock 这个参数,getTransactionReceipt() 才有 status ,用于判断交易执行结果
4) alloc 中指定了预分配 20个eth 的地址,其私钥是 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449
# 3. 初始化私有链
$ cd ~/.ethereum
$ geth --datadir ~/.ethereum/ init ./genesis.json
# 4. 账户基本操作
关于以太坊的私钥、公钥、地址、钱包的说明,参见第8节。
## 4.1 导入账户
$ echo 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449 > ~/.ethereum/coinbase.key
$ geth account import ~/.ethereum/coinbase.key
输入密码后,会在 ~/.ethereum/keystore/ 目录下生成一个账户文件,对应的地址就是创世文件里预分配了 20 eth 的那个 0xc1de867b55fdb749be0c927ecf7b19451777042b
## 4.2 生成新账户
执行以下命令,输入密码后会随机生成一个新的账户
$ geth account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {a90bbc1870e06a84b3dcf92a98f55453e6404a32}
## 4.3 列出账户
$ geth account list
Account #0: {c1de867b55fdb749be0c927ecf7b19451777042b} keystore:///data/www/.ethereum/keystore/UTC--2018-02-09T13-11-19.026113731Z--c1de867b55fdb749be0c927ecf7b19451777042b
Account #1: {7aa99ede746e863e87cdd1e686abadb31325cb92} keystore:///data/www/.ethereum/keystore/UTC--2018-02-18T09-00-05.982653587Z--7aa99ede746e863e87cdd1e686abadb31325cb92
# 5. 启动私有链
$ geth --identity "TestNode" --rpc --rpcport 8545 --port 30303 --nodiscover console
说明:
1) nodiscover 表示不会去连接其他节点(单节点私有链)
2) console 表示启动 geth 的 javascript 控制台
# 6. 控制台操作
在上一步启动的控制台里执行以下命令;或者通过 geth attach 连接到已经启动的 geth 实例执行。
## 6.1 查看账户
引用
> eth.accounts
["0xc1de867b55fdb749be0c927ecf7b19451777042b", "0x7aa99ede746e863e87cdd1e686abadb31325cb92"]
> eth.coinbase
"0xc1de867b55fdb749be0c927ecf7b19451777042b"
["0xc1de867b55fdb749be0c927ecf7b19451777042b", "0x7aa99ede746e863e87cdd1e686abadb31325cb92"]
> eth.coinbase
"0xc1de867b55fdb749be0c927ecf7b19451777042b"
一般来说 coinbase 账户(挖矿的收益账户)是第一个导入的账户,也就是 eth.accounts[0]。
## 6.2 查看余额
引用
> eth.getBalance(eth.coinbase)
20000000000000000000
20000000000000000000
## 6.3 转账
引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var ether = Math.pow(10, 18)
> var gwei = Math.pow(10, 9)
> tx_hash = eth.sendTransaction({from:eth.coinbase, to: eth.accounts[1], value: 1 * ether, gasPrice: 2 * gwei, gas: 21000, data: null})
"0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"
> eth.getTransaction(tx_hash)
{
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
blockNumber: null,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 21000,
gasPrice: 2000000000,
hash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
input: "0x",
nonce: 0,
r: "0xe99aa75fdb2d071632f7d7796229e3d84df60407007032f29b69296677df798c",
s: "0x616be42ff2a41a153f953685dc497d58ab4cca6ace4eac70405c611f078fcf4b",
to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
transactionIndex: 0,
v: "0x200024",
value: 1000000000000000000
}
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var ether = Math.pow(10, 18)
> var gwei = Math.pow(10, 9)
> tx_hash = eth.sendTransaction({from:eth.coinbase, to: eth.accounts[1], value: 1 * ether, gasPrice: 2 * gwei, gas: 21000, data: null})
"0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"
> eth.getTransaction(tx_hash)
{
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
blockNumber: null,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 21000,
gasPrice: 2000000000,
hash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
input: "0x",
nonce: 0,
r: "0xe99aa75fdb2d071632f7d7796229e3d84df60407007032f29b69296677df798c",
s: "0x616be42ff2a41a153f953685dc497d58ab4cca6ace4eac70405c611f078fcf4b",
to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
transactionIndex: 0,
v: "0x200024",
value: 1000000000000000000
}
说明:
1) 以太币的最小单位 wei = 10^-18 ether
2) sendTransaction 的几个参数说明:
from: 交易发起方地址,必填,需要先解锁
to: 交易接收方地址,转账交易必填
value: 转账金额,单位为 wei
gasPrice: gas的价格,选填(geth会用估算值自动填充)
gas: 交易消耗的最大gas数量,选填(转账交易正好消耗21000,少于这个数字geth会拒绝创建交易); 多出的部分会在交易成功以后返还
data: 交易的输入数据,选填(转账交易不需要)
3) getTransaction 根据交易的hash获取交易详情,包括sendTransaction给出的参数,签名数据,和包含该交易的区块信息。由于当前还没有被确认,所以blockHash和blockNumber都是无效数据;特别提一下,nonce 其实也是 sendTransaction 的一个参数,相当于由该账户发起的交易的订单号,以太坊要求nonce严格有序递增(即下一笔订单的nonce一定是上一笔的nonce+1)。
关于gas和gasPrice,参见第9节。
由于交易还未被确认,所以暂不能得知交易的执行结果。
## 6.4 挖矿
引用
> eth.blockNumber
0
> miner.start();
> admin.sleepBlocks(1);
> miner.stop();
> eth.blockNumber
1
> eth.getBlock(1)
{
difficulty: 131072,
extraData: "0xd783010802846765746887676f312e392e34856c696e7578",
gasLimit: 4076476,
gasUsed: 21000,
hash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
logsBloom: "0x000(省略部分)000",
miner: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
mixHash: "0x16639b8a9e2adaddbb9a48f1963b16ebdb997a528c4a1aeb4ac06e04501b56e9",
nonce: "0x1cf42797e7b2c1d8",
number: 1,
parentHash: "0xc75321a9915b2a2c973528f713a2a587e28244a032e3fcd46a5915112269c6a4",
receiptsRoot: "0xd95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 764,
stateRoot: "0x0de36a5d34f176a081fde3f26646b9368f28736ad01e96000c7ac05565213d53",
timestamp: 1521027698,
totalDifficulty: 35081353,
transactions: ["0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"],
transactionsRoot: "0x4ae833bcf2114a237b86755453117da68876888d8c4215da8fb750016d5470f6",
uncles: []
}
0
> miner.start();
> admin.sleepBlocks(1);
> miner.stop();
> eth.blockNumber
1
> eth.getBlock(1)
{
difficulty: 131072,
extraData: "0xd783010802846765746887676f312e392e34856c696e7578",
gasLimit: 4076476,
gasUsed: 21000,
hash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
logsBloom: "0x000(省略部分)000",
miner: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
mixHash: "0x16639b8a9e2adaddbb9a48f1963b16ebdb997a528c4a1aeb4ac06e04501b56e9",
nonce: "0x1cf42797e7b2c1d8",
number: 1,
parentHash: "0xc75321a9915b2a2c973528f713a2a587e28244a032e3fcd46a5915112269c6a4",
receiptsRoot: "0xd95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 764,
stateRoot: "0x0de36a5d34f176a081fde3f26646b9368f28736ad01e96000c7ac05565213d53",
timestamp: 1521027698,
totalDifficulty: 35081353,
transactions: ["0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"],
transactionsRoot: "0x4ae833bcf2114a237b86755453117da68876888d8c4215da8fb750016d5470f6",
uncles: []
}
## 6.5 查看交易执行结果
引用
> eth.getTransaction(tx_hash)
{
blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
blockNumber: 267,
...
}
> eth.getTransactionReceipt(tx_hash)
{
blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
blockNumber: 267,
contractAddress: null,
cumulativeGasUsed: 42000,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 21000,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
transactionHash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
transactionIndex: 1
}
> eth.getBalance(eth.accounts[1])
1000000000000000000
{
blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
blockNumber: 267,
...
}
> eth.getTransactionReceipt(tx_hash)
{
blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
blockNumber: 267,
contractAddress: null,
cumulativeGasUsed: 42000,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 21000,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
transactionHash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
transactionIndex: 1
}
> eth.getBalance(eth.accounts[1])
1000000000000000000
说明:
1) 交易已经被确认,所以 getTransaction 可以看到区块信息
2) getTransactionReceipt 得到交易回执,其中:
gasUsed: 实际消耗的gas(转账交易总是21000,除非指定了额外的data)
status: "0x1" 表示成功, "0x0" 表示失败
contractAddress: 新部署的合约地址;这个交易只是普通转账交易,不会部署合约,所以是null
logs: 记录发送给合约的交易触发的事件,普通转账交易不会产生事件,所以是 [] 。
# 7. 合约
以太坊的账户分成两种,一种是EOA账户(Externally Owned Account,由私钥控制),里面存储了账户的以太币余额信息和由该账户发起的交易数量(nonce),上面创建的都是EOA账户。另一种是合约账户(Contract Account),是由合约部署交易创建的,里面除了存储以太币余额,还存储了合约代码,以及代码的状态(存储)。通过创建并发送由某个EOA账户私钥签名的交易,可以从该账户给其他EOA账户转账,或者向某个合约账户发起一笔交易(调用合约账户里的某个方法);而合约账户只能被动触发,接受某个EOA账户(或其他被触发的合约账户)发起的交易,根据该交易的指令执行相应的动作,修改内部状态,或向其他账户发出交易。
所谓合约,并不是某个要被执行的合同,其实质是一段代码,可以类比为一个Class,其中定义了一些方法、状态、事件。将这段代码编译成以太坊虚拟机(Ethereum Virtual Machine,简称EVM)机器码,就可以作为 sendTransaction() 方法的 data 参数,被部署到以太坊区块链上(会自动分配一个地址),这样就创建了一个合约账户。后续按照EVM ABI约定发送到该合约账户的交易,可以指定触发这段代码的相应方法执行预定的操作。
这样的描述有点抽象,下面就用具体的栗子来说明合约账户。
## 7.1 ERC20
以太坊上线几年了,目前为止最成熟的应用就是发行 ERC20 Token ,圆了好多人的会所嫩模梦。所谓 ERC20 ,实际上就是一个 Interface ,凡是遵守这个 Interface 的合约,都可以被相同的方式触发。
具体的 ERC20 Interface 实现了多个方法和事件,详见Wiki:https://theethereum.wiki/w/index.php/ERC20_Token_Standard
下面的栗子里,只挑最重要的 balanceOf 和 transfer 这两个方法,以及 Transfer 这个事件:
引用
contract ERC20Interface {
function balanceOf(address tokenOwner) public constant returns (uint balance);
function transfer(address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
}
function balanceOf(address tokenOwner) public constant returns (uint balance);
function transfer(address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
}
从interface的定义可以比较直接的看懂,balanceOf用于查询某个地址的余额,tranfer用于转账到某个地址;如果转账成功,transfer方法会触发一个 Transfer 事件,记录在区块链上。
下面看一个具体的代码 (保存为 token.sol ):
引用
pragma solidity ^0.4.0; //指定solidity编译器的最小版本
contract MyToken {
string public name; //token的名称,例如 Tether USD
string public symbol; //token的代码,例如 USDT
uint8 public decimals; //token的精度,例如4表示该token精确到小数点后4位
mapping (address => uint256) public balances; //该合约的账本,记录某地址的token余额
event Transfer(address indexed from, address indexed to, uint256 value);
/* 合约的初始化函数,在部署合约时被调用,指定供应量等信息 */
function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) {
balances[msg.sender] = initialSupply;
name = tokenName;
symbol = tokenSymbol;
decimals = decimalUnits;
}
function transfer(address _to, uint256 _value) {
/* 检查发送方余额充足,且不会溢出 */
require(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]);
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value); //关注该合约的用户可以得知发生了一笔转账事件
}
function balanceOf(address tokenOwner) public constant returns (uint balance) {
return balances[tokenOwner]; //查询某个地址的token余额
}
}
contract MyToken {
string public name; //token的名称,例如 Tether USD
string public symbol; //token的代码,例如 USDT
uint8 public decimals; //token的精度,例如4表示该token精确到小数点后4位
mapping (address => uint256) public balances; //该合约的账本,记录某地址的token余额
event Transfer(address indexed from, address indexed to, uint256 value);
/* 合约的初始化函数,在部署合约时被调用,指定供应量等信息 */
function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) {
balances[msg.sender] = initialSupply;
name = tokenName;
symbol = tokenSymbol;
decimals = decimalUnits;
}
function transfer(address _to, uint256 _value) {
/* 检查发送方余额充足,且不会溢出 */
require(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]);
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value); //关注该合约的用户可以得知发生了一笔转账事件
}
function balanceOf(address tokenOwner) public constant returns (uint balance) {
return balances[tokenOwner]; //查询某个地址的token余额
}
}
## 7.2 编译合约
引用
$ solc token.sol --bin --abi
...(编译器warning等)...
======= token.sol:MyToken =======
Binary:
6060604052341{合约被编译后产生的EVM字节码}fd000029
Contract JSON ABI
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"},{"name":"decimalUnits","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]
...(编译器warning等)...
======= token.sol:MyToken =======
Binary:
6060604052341{合约被编译后产生的EVM字节码}fd000029
Contract JSON ABI
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"},{"name":"decimalUnits","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]
## 7.3 部署合约
从上面的编译结果 copy 字节码 和 abi json,执行下面的命令:
引用
$ geth attach #连接到 geth 的 console
> var code = '0x6060604052341{EVM字节码}fd000029'; //这里以字符串形式保存字节码
> var abi = [{"constant":true,....:"event"}]; //注意这里不是字符串,而是json数组
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var contract = eth.contract(abi).new(1000000, 'Test Token', 'TEST', 2, {from: eth.coinbase, data: code, gas: 1000000})
> contract.transactionHash
"0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c"
> var code = '0x6060604052341{EVM字节码}fd000029'; //这里以字符串形式保存字节码
> var abi = [{"constant":true,....:"event"}]; //注意这里不是字符串,而是json数组
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var contract = eth.contract(abi).new(1000000, 'Test Token', 'TEST', 2, {from: eth.coinbase, data: code, gas: 1000000})
> contract.transactionHash
"0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c"
创建合约交易:初始化参数、合约部署方、gas limit;因为没有接收方,也不需要发送以太币,所以省略to和value
挖矿确认交易,然后看看部署的结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> eth.getTransaction(contract.transactionHash)
{
blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
blockNumber: 2,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 1000000,
gasPrice: 1000000000,
hash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
input: "0x606060{EVM字节码}fd000029{合约初始化参数的ABI编码}",
nonce: 1,
r: "0x32c5cb0c44b03dd273b6973f59f85c8e09d0d6212a38263b3c199329dbbc9ef",
s: "0x758efe028c494fc8e637792aa3e09adceb1e7cdafb8567d4a7eecf95f54accc4",
to: null,
transactionIndex: 0,
v: "0x200024",
value: 0
}
> eth.getTransactionReceipt(contract.transactionHash)
{
blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
blockNumber: 2,
contractAddress: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
cumulativeGasUsed: 594886,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 594886,
logs: [],
logsBloom: "0x0000{省略一些0}0000",
status: "0x1",
to: null,
transactionHash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
transactionIndex: 0
}
> contract.address
"0x52ce1b10f214abb88517ceb3de2fbfbbfc305791"
true
> eth.getTransaction(contract.transactionHash)
{
blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
blockNumber: 2,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 1000000,
gasPrice: 1000000000,
hash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
input: "0x606060{EVM字节码}fd000029{合约初始化参数的ABI编码}",
nonce: 1,
r: "0x32c5cb0c44b03dd273b6973f59f85c8e09d0d6212a38263b3c199329dbbc9ef",
s: "0x758efe028c494fc8e637792aa3e09adceb1e7cdafb8567d4a7eecf95f54accc4",
to: null,
transactionIndex: 0,
v: "0x200024",
value: 0
}
> eth.getTransactionReceipt(contract.transactionHash)
{
blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
blockNumber: 2,
contractAddress: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
cumulativeGasUsed: 594886,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 594886,
logs: [],
logsBloom: "0x0000{省略一些0}0000",
status: "0x1",
to: null,
transactionHash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
transactionIndex: 0
}
> contract.address
"0x52ce1b10f214abb88517ceb3de2fbfbbfc305791"
可以看到,发送的交易在被挖矿确认以后,部署到了 0x52ce1b10f214abb88517ceb3de2fbfbbfc305791 这个地址,消耗了 594886 个 gas 。这样就“发行”了一个ERC20 Token,名字是 Test Token,代码 TEST ,一共发行 10000 个,每个 token 精确到小数点后 2 位。
## 7.4 调用合约
上面的 contract 变量就是合约对象了,但不能每次想调用一个合约都得创建它,通常我们是需要直接调用某个合约地址里的方法,那就需要用这种方式创建合约对象:
引用
> var abi = [{"contract"...}];
> var contract = eth.contract(abi).at("0x52ce1b10f214abb88517ceb3de2fbfbbfc305791")
> var contract = eth.contract(abi).at("0x52ce1b10f214abb88517ceb3de2fbfbbfc305791")
余额查询:
引用
> contract.balanceOf.call(eth.coinbase)
1000000
1000000
可以看到,由于我们是用 coinbase 帐号发起的交易,合约在初始化的时候,根据合约代码的实现,把初始发行的 10000 个 TEST 都记到了 coinbase 帐号名下。要特别注意的是,这个账,不是记在 coinbase 这个EOA帐号的存储里,而是记在这个合约帐号的存储(代码里balances这个map)里面。
转账(误):
引用
> contract.transfer.call(eth.accounts[1], 100)
[]
> contract.balanceOf(eth.accounts[1])
0
[]
> contract.balanceOf(eth.accounts[1])
0
可以看到,虽然调用了transfer方法,但是 eth.accounts[1] 的余额并没变化。这是因为transfer方法是要修改存储的,而以太坊上的状态,只能由被区块链确认了的交易出发的动作修改。
那就试试往区块链上发送一笔转账交易:
引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> tx_hash = contract.transfer.sendTransaction(eth.accounts[1], 100, {from:eth.coinbase, gas:100000})
"0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad"
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> tx_hash = contract.transfer.sendTransaction(eth.accounts[1], 100, {from:eth.coinbase, gas:100000})
"0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad"
挖矿确认这笔交易,然后查看执行结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> contract.balanceOf(eth.accounts[1])
100
> contract.balanceOf(eth.coinbase)
999900
> eth.getTransaction(tx_hash)
{
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 3,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 100000,
gasPrice: 1000000000,
hash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
input: "0xa9059cbb00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd0000000000000000000000000000000000000000000000000000000000000064",
nonce: 2,
r: "0x53b6312127838fe8cdca5209b51d33175f370b2a0921215d07f2df9a572c120d",
s: "0xd18471632a066386c42a42226f441442f73e0ba39b5a9b6db58a283d957579a",
to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
transactionIndex: 0,
v: "0x200024",
value: 0
}
> eth.getTransactionReceipt(tx_hash)
{
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 3,
contractAddress: null,
cumulativeGasUsed: 51595,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 51595,
logs: [{
address: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 271,
data: "0x0000000000000000000000000000000000000000000000000000000000000064",
logIndex: 0,
removed: false,
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b", "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
transactionIndex: 0
}],
logsBloom: "0x0000010{省略部分数据}0000000000000",
status: "0x1",
to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
transactionIndex: 0
}
true
> contract.balanceOf(eth.accounts[1])
100
> contract.balanceOf(eth.coinbase)
999900
> eth.getTransaction(tx_hash)
{
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 3,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gas: 100000,
gasPrice: 1000000000,
hash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
input: "0xa9059cbb00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd0000000000000000000000000000000000000000000000000000000000000064",
nonce: 2,
r: "0x53b6312127838fe8cdca5209b51d33175f370b2a0921215d07f2df9a572c120d",
s: "0xd18471632a066386c42a42226f441442f73e0ba39b5a9b6db58a283d957579a",
to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
transactionIndex: 0,
v: "0x200024",
value: 0
}
> eth.getTransactionReceipt(tx_hash)
{
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 3,
contractAddress: null,
cumulativeGasUsed: 51595,
from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
gasUsed: 51595,
logs: [{
address: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
blockNumber: 271,
data: "0x0000000000000000000000000000000000000000000000000000000000000064",
logIndex: 0,
removed: false,
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b", "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
transactionIndex: 0
}],
logsBloom: "0x0000010{省略部分数据}0000000000000",
status: "0x1",
to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
transactionIndex: 0
}
向区块链发送一个交易,等到交易被确认以后,再查询 eth.accounts[1] 的 token 余额,就可以看到发生了变化。
这里有几个比较重要的信息:
1) getTransaction 返回结果里的 input
之前的普通转账交易,input的内容(就是sendTransaction里指定的data)都是 "0x" ,也就是 0 字节,无数据。而这个交易,有了一个比较复杂的值。这个值可以被拆成3段:
0xa9059cbb
=> 按照evm abi的约定,这是合约方法的签名的前4个字节。签名是用sha3对方法的定义进行hash计算出来的
=> 例如这个 0xa9059cbb 就来源于 sha3("transfer(address,uint256)")
00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd
=> 这就是接收方的地址,但用32字节表示,前面用0补齐
0000000000000000000000000000000000000000000000000000000000000064
=> 这是转账金额,32字节无符号整数,0x64 = 100
2) getTransactionReceipt 返回结果里的 logs
logs是个数组,其中的每一项代表一个被触发的 event ,这个栗子里,logs[0].topics 包含3个元素:
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
=> 这是事件的签名,sha3("Transfer(address,address,uint256)")
"0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b"
=> 这是发送方地址,也就是 eth.coinbase
"0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
=> 这是接受方地址,也就是 eth.coinbase
因为 Transfer 事件的定义里,from和to都带了一个 indexed 修饰符,所以会被记录到 topics 里,而没带 indexed 修饰符的 value ,则被记录在了 logs[0].data 里。
# 8. 以太坊的私钥、公钥、地址、钱包
以太坊和比特币一样,都是用的椭圆曲线加密算法 ECDSA-secp256k1。通常将私钥记为 k ,公钥记为 K 。下面使用node,安装bitcore-lib来演示。
引用
$ node
> var bitcore = require("bitcore-lib")
> var bitcore = require("bitcore-lib")
## 8.1 私钥
私钥 k 通常是随机选出的 32 字节数据(可以看作一个 uint256 大整数),但要特别注意,不能使用伪随机数生成器。更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77,略小于2^256)。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。
引用
> var k = bitcore.PrivateKey()
> k.toBuffer().hexSlice()
'3f4d9a11b162c61af91c63e601b0dadaec09c74abb7f6659364549011b0dcf15'
> k.toBuffer().hexSlice()
'3f4d9a11b162c61af91c63e601b0dadaec09c74abb7f6659364549011b0dcf15'
## 8.2 公钥
将私钥 k 与一个指定的生成点 G 运算,就能得到公钥 K = k * G。K是椭圆曲线上的一个点 (x, y)。其中x和y都是uint256大整数。在以太坊里,公钥的二进制存储格式为 x + y,一共64字节。
引用
> var K = k.toPublicKey()
> K.point.x.toBuffer().hexSlice()
'fbc1994297bd2bdd5a4464ae7f6c88338587a175faffeb414fb2717207638435'
> K.point.y.toBuffer().hexSlice()
'b5d1a6015fc9fc6cf947046213cfb7d3341491934a90a2e3880af6a940cdd1df'
> var K_buffer = Buffer.concat([K.point.x.toBuffer(), K.point.y.toBuffer()])
> K.point.x.toBuffer().hexSlice()
'fbc1994297bd2bdd5a4464ae7f6c88338587a175faffeb414fb2717207638435'
> K.point.y.toBuffer().hexSlice()
'b5d1a6015fc9fc6cf947046213cfb7d3341491934a90a2e3880af6a940cdd1df'
> var K_buffer = Buffer.concat([K.point.x.toBuffer(), K.point.y.toBuffer()])
## 8.3 地址
通过对公钥K做sha3(keccak256)哈希,得到一个32字节的结果,取其中的后20字节,就是以太坊的地址了:
引用
> const SHA3 = require('keccakjs')
> addr = '0x' + SHA3(256).update(K_buffer).digest('hex').slice(-40)
'0x2864cdd53410820bc684f746871e9820fbb57243'
> addr = '0x' + SHA3(256).update(K_buffer).digest('hex').slice(-40)
'0x2864cdd53410820bc684f746871e9820fbb57243'
这样就得到了一个以太坊的地址。为了检测在地址传递过程中发生的错误,EIP55给出了一个校验码方案,其思路很简单,对地址再做一次sha3,根据哈希结果修改地址中对应位置的字符大小写,详细方案参见:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md。
加上校验码后,地址长这样:0x2864Cdd53410820BC684F746871E9820fBb57243
## 8.4 钱包
由于以太坊和比特币都用的是 secp256k1 ,所以很多以太坊钱包是参考BIP32+BIP44实现的(包括KeepKey这样的硬件钱包),其中Coin Type用的是60H。
详情参考BIP44:https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
# 9. Gas/GasLimit/GasPrice
为了避免死循环导致的拒绝服务攻击,比特币区块链的目标是实现一个点对点的电子现金系统,因此其内置的脚本不是图灵完备的,不支持循环,只能完成一些条件判断。
以太坊有更大的野心,实现了一个图灵完备的EVM。为了解决这个拒绝服务攻击的问题,以太坊实现了这样一个机制:为了让EVM这台机器运转起来,你需要给它提供“加密燃料”——gas。gas是用以太币计价的,因此在发送交易的时候,需要指定购买gas的价格——gasPrice。由于停机问题不可解,为了避免过度消耗,交易发送者还需要指定gasLimit(对应sendTransaction方法的gas参数),表示他愿意为这笔交易的执行支付的最大gas数量。
当EVM收到开始执行一个交易的时候,会先冻结 value + gas * gasPrice 这些以太币(转账金额 + 为这笔交易愿意支付的最大金额),然后开始执行交易。以太坊的黄皮书里定义了不同操作需要消耗的gas数量,EVM在执行过程中,会根据定义好的规则累计消耗的gas总量。如果交易尚未执行完,但所有gas都消耗完,EVM会回滚所有改动,并将交易标记为失败(getTransactionReceipt返回的status='0x0')。无论交易最终是否成功,EVM累计消耗的gas都会被扣除,记录在 receipt 的 gasUsed 字段里,剩余的gas则按gasPrice换成以太币,退给交易发送者。特别地,普通转账交易消耗的gas数量正好是21000。作为以太坊网络的维护者和验证者,挖出包含这个交易的block的矿工可以获得这些被消耗的gas。
因此,如果有人要用死循环来恶意攻击以太坊网络,他就要付出无限的以太币。
# 10. 其他
## 10.1 以太币的总量
以太坊正式上线时,发行了72002454个以太币,每年会按这个数字的25%增发(即不超过1800万)。截止到目前(2018-03-17),流通总量为9823万(数据来源于 coinmarketcap.com)。
## 10.2 大整数:十六进制
由于以太币的最小单位 wei = 1/10^18 ether,所以连 UINT64 都无法保存所有的以太币。很多语言没有提供原生的大整数支持,所以以太坊的rpc接口传递大整数时都使用16进制编码,由语言自行处理。
## 10.3 版本
以太坊最早于是2015年5月上线 Olympic 测试网,7月30日上线 Frontier 版本。Frontier是beta版本,供开发者学习、体验,直到2016年3月14日(圆周率日)上线了正式的 Homestead 版本。
以太坊后续还规划了 Metropolis 和 Serenity 两个版本,上线时间尚未确定。
## 10.4 挖矿和共识机制
Homestead 的共识机制与比特币一样,都是PoW(Proof of Work),但使用了一个基于DAG的Ethash算法,对内存的要求较高,因此像ASIC矿机这种大规模嵌入运算单元的方式无法加速挖矿效率,从而尽量避免挖矿的中心化,增加网络的安全性。
在接下来的Metropolis版本里,以太坊计划切换为PoS机制,提高出块的效率。
## 10.5 还有啥?
想起来再补吧。
欢迎扫码关注:
转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php 。
hake
2018-5-22 13:49
真牛。安利个以太坊dapp教程:http://xc.hubwiz.com/course/5a952991adb3847553d205d1?affid=20180522felix021
分页: 1/1 1