标题:以太坊区块链长什么样? —— 自建 ethereum 私有链指南 出处:Felix021 时间:Wed, 14 Mar 2018 19:55:06 +0000 作者:felix021 地址:https://www.felix021.com/blog/read.php?2186 内容: 之前接触以太坊的时候,确实能搜到很多资料,还有一个看起来很丰富的 Homestead Documentation,但这些材料都太不接地气了,看完还是不知道以太坊区块链到底长什么样,因此整理了这篇说明,希望能够在一定程度上解决这个问题吧。 # 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 # 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" } 说明: 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" 一般来说 coinbase 账户(挖矿的收益账户)是第一个导入的账户,也就是 eth.accounts[0]。 ## 6.2 查看余额 引用 > eth.getBalance(eth.coinbase) 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 } 说明: 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: [] } ## 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 说明: 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); } 从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余额 } } ## 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"}] ## 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" 创建合约交易:初始化参数、合约部署方、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" 可以看到,发送的交易在被挖矿确认以后,部署到了 0x52ce1b10f214abb88517ceb3de2fbfbbfc305791 这个地址,消耗了 594886 个 gas 。这样就“发行”了一个ERC20 Token,名字是 Test Token,代码 TEST ,一共发行 10000 个,每个 token 精确到小数点后 2 位。 ## 7.4 调用合约 上面的 contract 变量就是合约对象了,但不能每次想调用一个合约都得创建它,通常我们是需要直接调用某个合约地址里的方法,那就需要用这种方式创建合约对象: 引用 > var abi = [{"contract"...}]; > var contract = eth.contract(abi).at("0x52ce1b10f214abb88517ceb3de2fbfbbfc305791") 余额查询: 引用 > contract.balanceOf.call(eth.coinbase) 1000000 可以看到,由于我们是用 coinbase 帐号发起的交易,合约在初始化的时候,根据合约代码的实现,把初始发行的 10000 个 TEST 都记到了 coinbase 帐号名下。要特别注意的是,这个账,不是记在 coinbase 这个EOA帐号的存储里,而是记在这个合约帐号的存储(代码里balances这个map)里面。 转账(误): 引用 > contract.transfer.call(eth.accounts[1], 100) [] > 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" 挖矿确认这笔交易,然后查看执行结果: 引用 > 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 } 向区块链发送一个交易,等到交易被确认以后,再查询 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") ## 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' ## 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()]) ## 8.3 地址 通过对公钥K做sha3(keccak256)哈希,得到一个32字节的结果,取其中的后20字节,就是以太坊的地址了: 引用 > const SHA3 = require('keccakjs') > 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 还有啥? 想起来再补吧。 Generated by Bo-blog 2.1.0