diff --git a/Internal-ethapi b/Internal-ethapi new file mode 100644 index 0000000..b2349ef --- /dev/null +++ b/Internal-ethapi @@ -0,0 +1,213 @@ +在 internal/ethapi/api.go 中,可以通过 NewAccount 获取新账户,这个 api 可以通过交互式命令行或 rpc 接口调用。 + +func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) { + acc, err := fetchKeystore(s.am).NewAccount(password) + if err == nil { + return acc.Address, nil + } + return common.Address{}, err +} + +首先调用 fetchKeystore,通过 backends 获得 KeyStore 对象,最后通过调用 keystore.go 中的 NewAccount 获得新账户。 +func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { + _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) + if err != nil { + return accounts.Account{}, err + } + ks.cache.add(account) + ks.refreshWallets() + return account, nil +} +NewAccount 会调用 storeNewKey。 + +func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { + key, err := newKey(rand) + if err != nil { + return nil, accounts.Account{}, err + } + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}} + if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { + zeroKey(key.PrivateKey) + return nil, a, err + } + return key, a, err +} +注意第一个参数是 keyStore,这是一个接口类型。 + +type keyStore interface { + GetKey(addr common.Address, filename string, auth string) (*Key, error) + StoreKey(filename string, k *Key, auth string) error + JoinPath(filename string) string +} +storeNewKey 首先调用 newKey,通过椭圆曲线加密算法获取公私钥对。 + +func newKey(rand io.Reader) (*Key, error) { + privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand) + if err != nil { + return nil, err + } + return newKeyFromECDSA(privateKeyECDSA), nil +} +然后会根据参数 ks 的类型调用对应的实现,通过 geth account new 命令创建新账户,调用的就是 accounts/keystore/keystore_passphrase.go 中的实现。即 + +func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + return writeKeyFile(filename, keyjson) +} + +我们可以深入到 EncryptKey 中 +func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { + authArray := []byte(auth) + salt := randentropy.GetEntropyCSPRNG(32) + derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return nil, err + } + encryptKey := derivedKey[:16] + keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) + iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16 + cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv) + if err != nil { + return nil, err + } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) + scryptParamsJSON := make(map[string]interface{}, 5) + scryptParamsJSON["n"] = scryptN + scryptParamsJSON["r"] = scryptR + scryptParamsJSON["p"] = scryptP + scryptParamsJSON["dklen"] = scryptDKLen + scryptParamsJSON["salt"] = hex.EncodeToString(salt) + cipherParamsJSON := cipherparamsJSON{ + IV: hex.EncodeToString(iv), + } + cryptoStruct := cryptoJSON{ + Cipher: "aes-128-ctr", + CipherText: hex.EncodeToString(cipherText), + CipherParams: cipherParamsJSON, + KDF: keyHeaderKDF, + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + } + encryptedKeyJSONV3 := encryptedKeyJSONV3{ + hex.EncodeToString(key.Address[:]), + cryptoStruct, + key.Id.String(), + version, + } + return json.Marshal(encryptedKeyJSONV3) +} +EncryptKey 的 key 参数是加密的账户,包括 ID,公私钥,地址,auth 参数是用户输入的密码,scryptN 参数是 scrypt 算法中的 N,scryptP 参数是 scrypt 算法中的 P。整个过程,首先对密码使用 scrypt 算法加密,得到加密后的密码 derivedKey,然后用 derivedKey 对私钥使用 AES-CTR 算法加密,得到密文 cipherText,再对 derivedKey 和 cipherText 进行哈希运算得到 mac,mac 起到签名的作用,在解密的时候可以验证合法性,防止别人篡改。EncryptKey 最终返回 json 字符串,Storekey 方法接下来会将其保存在文件中。 + +列出所有账户 +列出所有账户的入口也在 internal/ethapi/api.go 里。 +func (s *PrivateAccountAPI) ListAccounts() []common.Address { + addresses := make([]common.Address, 0) // return [] instead of nil if empty + for _, wallet := range s.am.Wallets() { + for _, account := range wallet.Accounts() { + addresses = append(addresses, account.Address) + } + } + return addresses +} +该方法会从 Account Manager 中读取所有钱包信息,获取其对应的所有地址信息。 + +如果读者对 geth account 命令还有印象的话,geth account 命令还有 update,import 等方法,这里就不再讨论了。 + +发起转账 +发起一笔转账的函数入口在 internal/ethapi/api.go 中。 +func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { + account := accounts.Account{Address: args.From} + wallet, err := s.b.AccountManager().Find(account) + if err != nil { + return common.Hash{}, err + } + if args.Nonce == nil { + s.nonceLock.LockAddr(args.From) + defer s.nonceLock.UnlockAddr(args.From) + } + if err := args.setDefaults(ctx, s.b); err != nil { + return common.Hash{}, err + } + tx := args.toTransaction() + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := wallet.SignTx(account, tx, chainID) + if err != nil { + return common.Hash{}, err + } + return submitTransaction(ctx, s.b, signed) +} +转账时,首先利用传入的参数 from 构造一个 account,表示转出方。然后通过 accountMananger 的 Find 方法获得这个账户的钱包(Find 方法在上面有介绍),接下来有一个稍特别的地方。我们知道以太坊采用的是账户余额的体系,对于 UTXO 的方式来说,防止双花的方式很直观,一个输出不能同时被两个输入而引用,这种方式自然而然地就防止了发起转账时可能出现的双花,采用账户系统的以太坊没有这种便利,以太坊的做法是,每个账户有一个 nonce 值,它等于账户累计发起的交易数量,账户发起交易时,交易数据里必须包含 nonce,而且该值必须大于账户的 nonce 值,否则为非法,如果交易的 nonce 值减去账户的 nonce 值大于1,这个交易也不能打包到区块中,这确保了交易是按照一定的顺序执行的。如果有两笔交易有相同 nonce,那么其中只有一笔交易能够成功,通过给 nonce 加锁就是用来防止双花的问题。接着调用 args.setDefaults(ctx, s.b) 方法设置一些交易默认值。最后调用 toTransaction 方法创建交易: + +func (args *SendTxArgs) toTransaction() *types.Transaction { + var input []byte + if args.Data != nil { + input = *args.Data + } else if args.Input != nil { + input = *args.Input + } + if args.To == nil { + return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), uint64(*args.Gas), (*big.Int)(args.GasPrice), input) + } + return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), uint64(*args.Gas), (*big.Int)(args.GasPrice), input) +} +这里有两个分支,如果传入的交易的 to 参数不存在,那就表明这是一笔合约转账;如果有 to 参数,就是一笔普通的转账,深入后你会发现这两种转账最终调用的都是 newTransaction + +func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction { + return newTransaction(nonce, &to, amount, gasLimit, gasPrice, data) +} +func NewContractCreation(nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction { + return newTransaction(nonce, nil, amount, gasLimit, gasPrice, data) +} +newTransaction 的功能很简单,实际上就是返回一个 Transaction 实例。我们接着看 SendTransaction 方法接下来的部分。创建好一笔交易,接着我们通过 ChainConfig 方法获得区块链的配置信息,如果是 EIP155 里描述的配置,需要做特殊处理(待深入),然后调用 SignTx 对交易签名来确保这笔交易是真实有效的。SignTx 的接口定义在 accounts/accounts.go 中,这里我们看 keystore 的实现。 + +func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + ks.mu.RLock() + defer ks.mu.RUnlock() + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrLocked + } + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey) +} +首先验证账户是否已解锁,若没有解锁,直接报异常退出。接着根据 chainID 判断使用哪一种签名方式,调用相应 SignTx 方法进行签名。 + +func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) { + h := s.Hash(tx) + sig, err := crypto.Sign(h[:], prv) + if err != nil { + return nil, err + } + return tx.WithSignature(s, sig) +} +SignTx 的功能是调用椭圆加密函数获得签名,得到带签名的交易后,通过 SubmitTrasaction 提交交易。 + + +func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { + if err := b.SendTx(ctx, tx); err != nil { + return common.Hash{}, err + } + if tx.To() == nil { + signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) + from, err := types.Sender(signer, tx) + if err != nil { + return common.Hash{}, err + } + addr := crypto.CreateAddress(from, tx.Nonce()) + log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex()) + } else { + log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To()) + } + return tx.Hash(), nil +} +submitTransaction 首先调用 SendTx,这个接口在 internal/ethapi/backend.go 中定义,而实现在 eth/api_backend.go 中,这部分代码涉及到交易池,我们在单独的交易池章节进行探讨,这里就此打住。 + +将交易写入交易池后,如果没有因错误退出,submitTransaction 会完成提交交易,返回交易哈希值。发起交易的这个过程就结束了,剩下的就交给矿工将交易上链。 diff --git a/README.md b/README.md index 386ba4f..69d78db 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # go-ethereum-code-analysis - -**希望能够分析以太坊的代码来学习区块链技术和GO语言的使用** - -分析[go-ethereum](https://github.com/ethereum/go-ethereum)的过程,我希望从依赖比较少的底层技术组件开始,慢慢深入到核心逻辑。 +本文结合了一些网上资料,加上个人的原创结合而成。个人认为解释的比较清晰。若有疑问,还请及时批评指出。 ## 目录 - [go-ethereum代码阅读环境搭建](/go-ethereum源码阅读环境搭建.md) - [以太坊黄皮书 符号索引](a黄皮书里面出现的所有的符号索引.md) +- [account文件解析](/accounts源码分析.md) +- build文件解析: 此文件主要用于编译安装使用 +- [cmd文件解析](/cmd.md) +- [consensus文件解析](/consensus.md) - [rlp源码解析](/rlp源码解析.md) - [trie源码分析](/trie源码分析.md) - [ethdb源码分析](/ethdb源码分析.md) - [rpc源码分析](/rpc源码分析.md) - [p2p源码分析](/p2p源码分析.md) - [eth协议源码分析](/eth源码分析.md) -- core源码分析 +- core文件源码分析 + - [types文件解析](/types.md) + - [core/genesis.go](/core-genesis创世区块源码分析.md) + - [core/blockchain.go](/core-blockchain源码分析.md) - [区块链索引 chain_indexer源码分析](/core-chain_indexer源码解析.md) - [布隆过滤器索引 bloombits源码分析](/core-bloombits源码分析.md) - [以太坊的trie树管理 回滚等操作 state源码分析](/core-state源码分析.md) @@ -27,7 +31,6 @@ - [交易执行和处理部分源码分析](/core-txlist交易池的一些数据结构源码分析.md) - [交易执行和处理部分源码分析](/core-txpool交易池源码分析.md) - [创世区块的源码分析](/core-genesis创世区块源码分析.md) - - [blockchain 源码分析](/core-blockchain源码分析.md) - [miner挖矿部分源码分析CPU挖矿](/miner挖矿部分源码分析CPU挖矿.md) - [pow一致性算法](/pow一致性算法.md) - [以太坊测试网络Clique_PoA介绍](/以太坊测试网络Clique_PoA介绍.md) diff --git "a/accounts\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/accounts\346\272\220\347\240\201\345\210\206\346\236\220.md" index 9f69037..ef63ff4 100644 --- "a/accounts\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/accounts\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -1,10 +1,64 @@ -accounts包实现了以太坊客户端的钱包和账户管理。以太坊的钱包提供了keyStore模式和usb两种钱包。同时以太坊的 合约的ABI的代码也放在了account/abi目录。 abi项目好像跟账户管理没有什么关系。 这里暂时只分析了账号管理的接口。 具体的keystore和usb的实现代码暂时不会给出。 +accounts包实现了以太坊客户端的钱包和账户管理。以太坊的钱包提供了keyStore模式和usb两种钱包。同时以太坊的 合约的ABI的代码也放在了account/abi目录。 这里暂时只分析了账号管理的接口。 具体的keystore和usb的实现代码暂时不会给出。 +# 组件关系 +![](https://github.com/Billy1900/go-ethereum-code-analysis/blob/master/picture/accounts.png) +# accounts支持的钱包类型 +在accounts中总共支持两大类共4种钱包类型。两大类包括keystore和usbwallet;其中keystore中的私钥存储可以分为加密的和不加密的;usbwallet支持ledger和trenzer两种硬件钱包。 +# keystore:本地文件夹 +keystore类型的钱包其实是一个本地文件夹目录。在这个目录下可以存放多个文件,每个文件都存储着一个私钥信息。这些文件都是json格式,其中的私钥可以是加密的,也可以是非加密的明文。但非加密的格式已经被废弃了(谁都不想把自己的私钥明文存放在某个文件里)。 keystore的目录路径可以在配置文件中指定,默认路径是/keystore。每一个文件的文件名格式为:UTC----
。例如UTC--2016-03-22T12-57-55-- 7ef5a6135f1fd6a02593eedc869c6d41d934aef8。 keystore目录和目录内的文件是可以直接拷贝的。也就是说,如果你想把某个私钥转移到别的电脑上,你可以直接拷贝文件到其它电脑的keystore目录。拷贝整个keystore目录也是一样的。 +# HD:分层确定性(Hierarchical Deterministic)钱包 +我们首先解释一下HD(Hierarchical Deterministic)的概念。这个概念的中文名称叫做“分层确定性”,我的理解是这是一种key的派生方式,它可以在只使用一个公钥(我们称这个公钥为主公钥,其对应的私钥称为主私钥)的情况下,生成任意多个子公钥,而这些子公钥都是可以被主私钥控制的。HD的概念最早是从比特币的BIP-32提案中提出来的。每一个key都有自己的路径,即是是一个派生的key,这一点和keystore类型是一样的。我们先来看一下HD账户的路径格式: + m / purpose’ / coin_type’ / account’ / change / address_index +这种路径规范不是一下子形成的。虽然BIP-32提出了HD的概念,但实现者自由度比较大,导致相互之间兼容性很差。因此在BIP-43中增加了purpose字段;而在BIP-44中对路径规范进行了大量的扩展,使其可以用在不同币种上。在BIP-43中推荐purpose的值为44’(0x8000002C);而在BIPSLIP-44中为以太坊类型的coin_type为配的值为60’(0x8000003c)。所以我们在以太坊中可能看到形如m/44'/60'/0'/0这样的路径。在accounts模块中共支持两种HD钱包:Ledger和Trenzer。它们都是非常有名的硬件钱包,有兴趣的朋友可以自己搜索一下,这是不作过多介绍。 +# 目录结构 +accounts模块下的源文件比较多,这里不一一说明,只挑一些比较重要的聊一下。 +### accounts.go +accounts.go定义了accounts模块对外导出的一些结构体和接口,包括Account结构体、Wallet接口和Backend接口。其中Account由一个以太坊地址和钱包路径组成;而各种类型的钱包需要实现Wallet和Backend接口来接入账入管理。 +### hd.go +hd.go中定义了HD类型的钱包的路径解析等函数。这个文件中的注释还解析了HD路径一些知识,值得一看。(但我认为它关于哪个BIP提案提出的哪个规范说得不对,比如注释中提到BIP-32定义了路径规范m / purpose' / coin_type' / account' / change / address_index,这应该是错误的,我们前面提到过,purpose是在BIP-43中提出的,而整个路径规范是在BIP-44中提出的) +### manager.go +manager.go中定义了Manager结构及其方法。这是accounts模块对外导出的主要的结构和方法之一。其它模块(比如cmd/geth中)通过这个结构体提供的方法对钱包进行管理。 +### url.go +这个文件中的代码定义了代表以太坊钱包路径的URL结构体及相关函数。与hd.go中不同的是,URL结构体中保存了钱包的类型(scheme)和钱包路径的字符串形式的表示;而hd.go中定义了HD钱包路径的类型(非字符串类型)的解析及字符串转换等方法。
+## keystore +这是一个子目录,此目录下的代码实现了keystore类型的钱包。 +### account_cache.go +此文件中的代码实现了accountCache结构体及方法。accountCache的功能是在内存中缓存keystore钱包目录下所有账号信息。无论keystore目录中的文件无何变动(新建、删除、修改),accountCache都可以在扫描目录时将变动更新到内存中。 +### file_cache.go +此文件中的代码实现了fileCache结构体及相关代码。与account_cache.go类似,file_cache.go中实现了对keystore目录下所有文件的信息的缓存。accountCache就是通过fileCache来获取文件变动的信息,进而得到账号变动信息的。 +### key.go +key.go主要定义了Key结构体及其json格式的marshal/unmarshal方式。另外这个文件中还定义了通过keyStore接口将Key写入文件中的函数。keyStore接口中定义了Key被写入文件的具体细节,在passphrase.go和plain.go中都有实现。 +### keystore.go +这个文件里的代码定义了KeyStore结构体及其方法。KeyStore结构体实现了Backend接口,是keystore类型的钱包的后端实现。同时它也实现了keystore类型钱包的大多数功能。 +### passphrase.go +passphrase.go中定义了keyStorePassphrase结构体及其方法。keyStorePassphrase结构体是对keyStore接口(在key.go文件中)的一种实现方式,它会要求调用者提供一个密码,从而使用aes加密算法加密私钥后,将加密数据写入文件中。 +### plain.go +这个文件中的代码定义了keyStorePlain结构体及其方法。keyStorePlain与keyStorePassphrase类似,也是对keyStore接口的实现。不同的是,keyStorePlain直接将密码明文存储在文件中。目前这种方式已被标记弃用且整个以太坊项目中都没有调用这个文件里的函数的地方,确实谁也不想将自己的私钥明文存在本地磁盘上。 +### wallet.go +wallet.go中定义了keystoreWallet结构体及其方法。keystoreWallet是keystore类型的钱包的实现,但其功能基本都是调用KeyStore对象实现的。 +### watch.go +watch.go中定义了watcher结构体及其方法。watcher用来监控keystore目录下的文件,如果文件发生变化,则立即调用account_cache.go中的代码重新扫描账户信息。但watcher只在某些系统下有效,这是文件的build注释:// +build darwin,!ios freebsd linux,!arm64 netbsd solaris
+## usbwallet +这是一个子目录,此目录下的代码实现了对通过usb接入的硬件钱包的访问,但只支持ledger和trezor两种类型的硬件钱包。 +### hub.go +hub.go中定义了Hub结构体及其方法。Hub结构体实现了Backend接口,是usbwallet类型的钱包的后端实现。 +### ledger.go +ledger.go中定义了ledgerDriver结构体及其方法。ledgerDriver结构体是driver接口的实现,它实现了与ledger类型的硬件钱包通信协议和代码。 +### trezor.go +trezor.go中定义了trezorDriver结构体及其方法。与ledgerDriver类似,trezorDriver结构体也是driver接口的实现,它实现了与trezor类型的硬件钱包的通信协议和代码。 +### wallet.go +wallet.go中定义了wallet结构体。wallet结构体实现了Wallet接口,是硬件钱包的具体实现。但它内部其实主要调用硬件钱包的driver实现相关功能。 +## scwallet +这个文件夹是关于不同account之间的互相安全通信(secure wallet),通过定义会话秘钥、二级秘钥来确保通话双方的信息真实、不被篡改、利用。 尤其是转账信息更不能被利用、被他人打开、和被篡改。 +## backend +此文件夹是为了和外部的其他账户进行通信 +## abi +ABI是Application Binary Interface的缩写,字面意思 应用二进制接口,可以通俗的理解为合约的接口说明。当合约被编译后,那么它的abi也就确定了。abi主要是处理智能合约与账户的交互。 +
-账号是通过数据结构和接口来定义了 -## 数据结构 +# 数据结构 账号 // Account represents an Ethereum account located at a specific location defined @@ -158,7 +212,7 @@ accounts包实现了以太坊客户端的钱包和账户管理。以太坊的钱 // 订阅创建异步订阅,以便在后端检测到钱包的到达或离开时接收通知。 Subscribe(sink chan<- WalletEvent) event.Subscription } - +Backend 接口是一个钱包 provider,它包含一个钱包列表,在检测到钱包开启或关闭时可以接收到通知,可以用来请求签名交易。其中 Wallets() 返回当前可用的钱包,按字母顺序排序,Subscribe() 创建异步订阅的方法,当钱包发生变动时通过 chan 接收消息。 ## manager.go Manager是一个包含所有东西的账户管理工具。 可以和所有的Backends来通信来签署交易。 @@ -182,7 +236,7 @@ Manager是一个包含所有东西的账户管理工具。 可以和所有的Bac quit chan chan error lock sync.RWMutex } - +其中 backends 是当前已注册的所有 Backend,updaters 是所有 Backend 的更新订阅器,updates 是 Backend 对应 wallet 事件更新的 chan,wallets 是所有已经注册的 Backends 的钱包的缓存,feed 用于钱包事件的通知,quit 用于退出的事件。manager.go 的代码没有什么很特别的地方,有兴趣的话可以自行查看源代码,这里只做概述。这里只挑几个典型的,下面讲解业务实例时会用到的方法。 创建Manager @@ -218,8 +272,9 @@ Manager是一个包含所有东西的账户管理工具。 可以和所有的Bac return am } +NewManager 会将所有 backends 的 wallets 收集起来,获取所有的 backends 的时间订阅,然后根据这些参数创建新的 manager。 -update方法。 是一个goroutine。会监听所有backend触发的更新信息。 然后转发给feed. +update在 NewManager 作为一个 goroutine 被调用,一直运行,监控所有 backend 触发的更新消息,发给 feed 用来进行进一步的处理。 // update is the wallet event loop listening for notifications from the backends // and updating the cache of wallets. @@ -276,7 +331,7 @@ update方法。 是一个goroutine。会监听所有backend触发的更新信息 } -对于node来说。是什么时候创建的账号管理器。 +对node来说是什么时候创建的账号管理器 // New creates a new P2P node, ready for protocol registration. func New(conf *Config) (*Node, error) { @@ -343,3 +398,26 @@ update方法。 是一个goroutine。会监听所有backend触发的更新信息 } return accounts.NewManager(backends...), ephemeral, nil } + +首先获取配置信息,通过 getPassPhrase 获取密码后,通过 keystore.StoreKey 获得账户地址 +

+unc accountCreate(ctx *cli.Context) error {
+	cfg := gethConfig{Node: defaultNodeConfig()}
+	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
+		if err := loadConfig(file, &cfg); err != nil {
+			utils.Fatalf("%v", err)
+		}
+	}
+	utils.SetNodeConfig(ctx, &cfg.Node)
+	scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
+	if err != nil {
+		utils.Fatalf("Failed to read configuration: %v", err)
+	}
+	password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
+	address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
+	if err != nil {
+		utils.Fatalf("Failed to create account: %v", err)
+	}
+	fmt.Printf("Address: {%x}\n", address)
+	return nil
+}
diff --git a/cmd.md b/cmd.md new file mode 100644 index 0000000..817def9 --- /dev/null +++ b/cmd.md @@ -0,0 +1,635 @@ +# Cmd +## File structure +|文件|package|说明| +|-----|----------|-----------------------------------------------------------------------------------| +|cmd | |命令行工具,下面又分了很多的命令行工具| +|cmd |abigen 将智能合约源代码转换成容易使用的,编译时类型安全的Go语言包| +|cmd |bootnode |启动一个仅仅实现网络发现的节点| +|cmd | checkpoint-admin| checkpoint-admin is a utility that can be used to query checkpoint information and register stable checkpoints into an oracle contract.| +|cmd | clef | Clef is an account management tool| +|cmd | devp2p | ethereum p2p tool| +|cmd | ethkey | an Ethereum key manager| +|cmd | evm |以太坊虚拟机的开发工具, 用来提供一个可配置的,受隔离的代码调试环境| +|cmd | faucet |faucet is a Ether faucet backend by a light client.| +|cmd |geth |以太坊命令行客户端,最重要的一个工具| +|cmd |p2psim |提供了一个工具来模拟http的API| +|cmd |puppeth |创建一个新的以太坊网络的向导,一个命令组装和维护私人网路| +|cmd |rlpdump |提供了一个RLP数据的格式化输出| +|cmd |swarm |swarm网络的接入点| +|cmd |util |提供了一些公共的工具,为Go-Ethereum命令提供说明| +|cmd |wnode |这是一个简单的Whisper节点。 它可以用作独立的引导节点。此外,可以用于不同的测试和诊断目的。| + +## Cmd/geth +geth是cmd中最重要的命令,他是以太坊的入口。geth的命令行是通过urfave/cli这个库进行实现的,通过这个库,我们可以轻松定义命令行程序的子命令,命令选项,命令参数,描述信息等等。 + +geth 模块的入口在 cmd/geth/main.go 中,它会调用 urfave/cli 的中 app 的 run 方法,而 app 在 init 函数中初始化,在 Golang 中,如果有 init 方法,那么会在 main 函数之前执行 init 函数,它用于程序执行前的初始化工作。在 geth 模块中,init() 函数定义了命令行的入口是 geth,并且定义了 geth 的子命令、全局的命令选项、子命令的命令选项,按照 urfave/cli 的做法,不输入子命令会默认调用 geth,而 geth 方法其实就6行: +
func geth(ctx *cli.Context) error {
+	node := makeFullNode(ctx)
+	startNode(ctx, node)
+	node.Wait()
+	return nil
+}
+它会调用 makeFullNode 函数初始化一个全节点,接着通过 startNode 函数启动一个全节点,以阻塞的方式运行,等待着节点被终止。 +
func makeFullNode(ctx *cli.Context) *node.Node {
+	stack, cfg := makeConfigNode(ctx)
+	utils.RegisterEthService(stack, &cfg.Eth)
+	if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
+		utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
+	}
+	// whether enable whisper ...
+	// whether register eth stats ...
+	return stack
+}
+makeFullNode核心的逻辑是首先通过配置文件和 flag 生成系统级的配置,然后将服务注入到节点。 +
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
+	cfg := gethConfig{
+		Eth:       eth.DefaultConfig,
+		Shh:       whisper.DefaultConfig,
+		Node:      defaultNodeConfig(),
+		Dashboard: dashboard.DefaultConfig,
+	}
+	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
+		if err := loadConfig(file, &cfg); err != nil {
+			utils.Fatalf("%v", err)
+		}
+	}
+	utils.SetNodeConfig(ctx, &cfg.Node)
+	stack, err := node.New(&cfg.Node)
+	if err != nil {
+		utils.Fatalf("Failed to create the protocol stack: %v", err)
+	}
+	utils.SetEthConfig(ctx, stack, &cfg.Eth)
+	if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
+		cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
+	}
+	utils.SetShhConfig(ctx, stack, &cfg.Shh)
+	utils.SetDashboardConfig(ctx, &cfg.Dashboard)
+	return stack, cfg
+}
+makeConfigNode 会先载入默认配置,再载入配置文件中的配置,然后通过上下文的配置(在 cmd/geth/main.go 中的 init 方法中定义)进行设置。 +
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
+	var err error
+	if cfg.SyncMode == downloader.LightSync {
+		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
+			return les.New(ctx, cfg)
+		})
+	} else {
+		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
+			fullNode, err := eth.New(ctx, cfg)
+			if fullNode != nil && cfg.LightServ > 0 {
+				ls, _ := les.NewLesServer(fullNode, cfg)
+				fullNode.AddLesServer(ls)
+			}
+			return fullNode, err
+		})
+	}
+	if err != nil {
+		Fatalf("Failed to register the Ethereum service: %v", err)
+	}
+}
+RegisterEthService 的代码在 cmd/utils/flags.go 中,如果同步模式是轻量级同步模式,启动轻量级客户端,否则启动全节点,实际的注册方法是 stack.Register。注入服务其实就是将新的服务注入到 node 对象的 serviceFuncs 数组中。 +### geth/main.go +
func startNode(ctx *cli.Context, stack *node.Node) {
+	debug.Memsize.Add("node", stack)
+	utils.StartNode(stack)
+	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
+	passwords := utils.MakePasswordList(ctx)
+	unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
+	for i, account := range unlocks {
+		if trimmed := strings.TrimSpace(account); trimmed != "" {
+			unlockAccount(ctx, ks, trimmed, i, passwords)
+		}
+	}
+	events := make(chan accounts.WalletEvent, 16)
+	stack.AccountManager().Subscribe(events)
+	go func() {
+		rpcClient, err := stack.Attach()
+		if err != nil {
+			utils.Fatalf("Failed to attach to self: %v", err)
+		}
+		stateReader := ethclient.NewClient(rpcClient)
+		for _, wallet := range stack.AccountManager().Wallets() {
+			if err := wallet.Open(""); err != nil {
+				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
+			}
+		}
+		for event := range events {
+			switch event.Kind {
+			case accounts.WalletArrived:
+				if err := event.Wallet.Open(""); err != nil {
+					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
+				}
+			case accounts.WalletOpened:
+				status, _ := event.Wallet.Status()
+				log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
+				if event.Wallet.URL().Scheme == "ledger" {
+					event.Wallet.SelfDerive(accounts.DefaultLedgerBaseDerivationPath, stateReader)
+				} else {
+					event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
+				}
+			case accounts.WalletDropped:
+				log.Info("Old wallet dropped", "url", event.Wallet.URL())
+				event.Wallet.Close()
+			}
+		}
+	}()
+	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
+		if ctx.GlobalBool(utils.LightModeFlag.Name) || ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
+			utils.Fatalf("Light clients do not support mining")
+		}
+		var ethereum *eth.Ethereum
+		if err := stack.Service(ðereum); err != nil {
+			utils.Fatalf("Ethereum service not running: %v", err)
+		}
+		if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
+			type threaded interface {
+				SetThreads(threads int)
+			}
+			if th, ok := ethereum.Engine().(threaded); ok {
+				th.SetThreads(threads)
+			}
+		}
+		ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
+		if err := ethereum.StartMining(true); err != nil {
+			utils.Fatalf("Failed to start mining: %v", err)
+		}
+	}
+}
+startNode 方法启动节点,会开启所有已经注册的协议,解锁请求的账户,开启 RPC/IPC 接口,并开始挖矿。 +### geth/chaincmd.go +
func initGenesis(ctx *cli.Context) error {
+	genesisPath := ctx.Args().First()
+	if len(genesisPath) == 0 {
+		utils.Fatalf("Must supply path to genesis JSON file")
+	}
+	file, err := os.Open(genesisPath)
+	if err != nil {
+		utils.Fatalf("Failed to read genesis file: %v", err)
+	}
+	defer file.Close()
+	genesis := new(core.Genesis)
+	if err := json.NewDecoder(file).Decode(genesis); err != nil {
+		utils.Fatalf("invalid genesis file: %v", err)
+	}
+	stack := makeFullNode(ctx)
+	for _, name := range []string{"chaindata", "lightchaindata"} {
+		chaindb, err := stack.OpenDatabase(name, 0, 0)
+		if err != nil {
+			utils.Fatalf("Failed to open database: %v", err)
+		}
+		_, hash, err := core.SetupGenesisBlock(chaindb, genesis)
+		if err != nil {
+			utils.Fatalf("Failed to write genesis block: %v", err)
+		}
+		log.Info("Successfully wrote genesis state", "database", name, "hash", hash)
+	}
+	return nil
+}
+initCommand会进行初始化,生成初始区块,调用了SetupGenesisBlock +
func importChain(ctx *cli.Context) error {
+	if len(ctx.Args()) < 1 {
+		utils.Fatalf("This command requires an argument.")
+	}
+	stack := makeFullNode(ctx)
+	chain, chainDb := utils.MakeChain(ctx, stack)
+	defer chainDb.Close()
+	var peakMemAlloc, peakMemSys uint64
+	go func() {
+		stats := new(runtime.MemStats)
+		for {
+			runtime.ReadMemStats(stats)
+			if atomic.LoadUint64(&peakMemAlloc) < stats.Alloc {
+				atomic.StoreUint64(&peakMemAlloc, stats.Alloc)
+			}
+			if atomic.LoadUint64(&peakMemSys) < stats.Sys {
+				atomic.StoreUint64(&peakMemSys, stats.Sys)
+			}
+			time.Sleep(5 * time.Second)
+		}
+	}()
+	start := time.Now()
+	if len(ctx.Args()) == 1 {
+		if err := utils.ImportChain(chain, ctx.Args().First()); err != nil {
+			log.Error("Import error", "err", err)
+		}
+	} else {
+		for _, arg := range ctx.Args() {
+			if err := utils.ImportChain(chain, arg); err != nil {
+				log.Error("Import error", "file", arg, "err", err)
+			}
+		}
+	}
+	chain.Stop()
+	fmt.Printf("Import done in %v.\n\n", time.Since(start))
+	db := chainDb.(*ethdb.LDBDatabase)
+	stats, err := db.LDB().GetProperty("leveldb.stats")
+	if err != nil {
+		utils.Fatalf("Failed to read database stats: %v", err)
+	}
+	fmt.Println(stats)
+	ioStats, err := db.LDB().GetProperty("leveldb.iostats")
+	if err != nil {
+		utils.Fatalf("Failed to read database iostats: %v", err)
+	}
+	fmt.Println(ioStats)
+	fmt.Printf("Trie cache misses:  %d\n", trie.CacheMisses())
+	fmt.Printf("Trie cache unloads: %d\n\n", trie.CacheUnloads())
+	mem := new(runtime.MemStats)
+	runtime.ReadMemStats(mem)
+	fmt.Printf("Object memory: %.3f MB current, %.3f MB peak\n", float64(mem.Alloc)/1024/1024, float64(atomic.LoadUint64(&peakMemAlloc))/1024/1024)
+	fmt.Printf("System memory: %.3f MB current, %.3f MB peak\n", float64(mem.Sys)/1024/1024, float64(atomic.LoadUint64(&peakMemSys))/1024/1024)
+	fmt.Printf("Allocations:   %.3f million\n", float64(mem.Mallocs)/1000000)
+	fmt.Printf("GC pause:      %v\n\n", time.Duration(mem.PauseTotalNs))
+	if ctx.GlobalIsSet(utils.NoCompactionFlag.Name) {
+		return nil
+	}
+	start = time.Now()
+	fmt.Println("Compacting entire database...")
+	if err = db.LDB().CompactRange(util.Range{}); err != nil {
+		utils.Fatalf("Compaction failed: %v", err)
+	}
+	fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
+	stats, err = db.LDB().GetProperty("leveldb.stats")
+	if err != nil {
+		utils.Fatalf("Failed to read database stats: %v", err)
+	}
+	fmt.Println(stats)
+	ioStats, err = db.LDB().GetProperty("leveldb.iostats")
+	if err != nil {
+		utils.Fatalf("Failed to read database iostats: %v", err)
+	}
+	fmt.Println(ioStats)
+	return nil
+}
+ImportChain导入了一个区块链文件 +
func exportChain(ctx *cli.Context) error {
+	if len(ctx.Args()) < 1 {
+		utils.Fatalf("This command requires an argument.")
+	}
+	stack := makeFullNode(ctx)
+	chain, _ := utils.MakeChain(ctx, stack)
+	start := time.Now()
+	var err error
+	fp := ctx.Args().First()
+	if len(ctx.Args()) < 3 {
+		err = utils.ExportChain(chain, fp)
+	} else {
+		first, ferr := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
+		last, lerr := strconv.ParseInt(ctx.Args().Get(2), 10, 64)
+		if ferr != nil || lerr != nil {
+			utils.Fatalf("Export error in parsing parameters: block number not an integer\n")
+		}
+		if first < 0 || last < 0 {
+			utils.Fatalf("Export error: block number must be greater than 0\n")
+		}
+		err = utils.ExportAppendChain(chain, fp, uint64(first), uint64(last))
+	}
+	if err != nil {
+		utils.Fatalf("Export error: %v\n", err)
+	}
+	fmt.Printf("Export done in %v\n", time.Since(start))
+	return nil
+}
+exportCommand导出一个区块链gz文件 +
func importPreimages(ctx *cli.Context) error {
+	if len(ctx.Args()) < 1 {
+		utils.Fatalf("This command requires an argument.")
+	}
+	stack := makeFullNode(ctx)
+	diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)
+	start := time.Now()
+	if err := utils.ImportPreimages(diskdb, ctx.Args().First()); err != nil {
+		utils.Fatalf("Export error: %v\n", err)
+	}
+	fmt.Printf("Export done in %v\n", time.Since(start))
+	return nil
+}
+将一个preimages导入当前节点 +
func exportPreimages(ctx *cli.Context) error {
+	if len(ctx.Args()) < 1 {
+		utils.Fatalf("This command requires an argument.")
+	}
+	stack := makeFullNode(ctx)
+	diskdb := utils.MakeChainDatabase(ctx, stack).(*ethdb.LDBDatabase)
+	start := time.Now()
+	if err := utils.ExportPreimages(diskdb, ctx.Args().First()); err != nil {
+		utils.Fatalf("Export error: %v\n", err)
+	}
+	fmt.Printf("Export done in %v\n", time.Since(start))
+	return nil
+}
+从当前节点导出一个 image +
func copyDb(ctx *cli.Context) error {
+	if len(ctx.Args()) != 1 {
+		utils.Fatalf("Source chaindata directory path argument missing")
+	}
+	stack := makeFullNode(ctx)
+	chain, chainDb := utils.MakeChain(ctx, stack)
+	syncmode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode)
+	dl := downloader.New(syncmode, chainDb, new(event.TypeMux), chain, nil, nil)
+	db, err := ethdb.NewLDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name), 256)
+	if err != nil {
+		return err
+	}
+	hc, err := core.NewHeaderChain(db, chain.Config(), chain.Engine(), func() bool { return false })
+	if err != nil {
+		return err
+	}
+	peer := downloader.NewFakePeer("local", db, hc, dl)
+	if err = dl.RegisterPeer("local", 63, peer); err != nil {
+		return err
+	}
+	start := time.Now()
+	currentHeader := hc.CurrentHeader()
+	if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncmode); err != nil {
+		return err
+	}
+	for dl.Synchronising() {
+		time.Sleep(10 * time.Millisecond)
+	}
+	fmt.Printf("Database copy done in %v\n", time.Since(start))
+	start = time.Now()
+	fmt.Println("Compacting entire database...")
+	if err = chainDb.(*ethdb.LDBDatabase).LDB().CompactRange(util.Range{}); err != nil {
+		utils.Fatalf("Compaction failed: %v", err)
+	}
+	fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
+	return nil
+}
+复制一个本地区块文件到文件夹;在一个文件夹中创建一个本地区块链,但是这个过程并不是直接复制过去的,而是通过 downloader 模块里的 NewFakePeer 创建一个虚拟对等节点,然后再进行数据同步完成的。 +
func removeDB(ctx *cli.Context) error {
+	stack, _ := makeConfigNode(ctx)
+	for _, name := range []string{"chaindata", "lightchaindata"} {
+		logger := log.New("database", name)
+		dbdir := stack.ResolvePath(name)
+		if !common.FileExist(dbdir) {
+			logger.Info("Database doesn't exist, skipping", "path", dbdir)
+			continue
+		}
+		fmt.Println(dbdir)
+		confirm, err := console.Stdin.PromptConfirm("Remove this database?")
+		switch {
+		case err != nil:
+			utils.Fatalf("%v", err)
+		case !confirm:
+			logger.Warn("Database deletion aborted")
+		default:
+			start := time.Now()
+			os.RemoveAll(dbdir)
+			logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start)))
+		}
+	}
+	return nil
+}
+在当前数据库中移除区块链,删除数据库是直接通过 os 模块移除这个文件夹。 +
func dump(ctx *cli.Context) error {
+	stack := makeFullNode(ctx)
+	chain, chainDb := utils.MakeChain(ctx, stack)
+	for _, arg := range ctx.Args() {
+		var block *types.Block
+		if hashish(arg) {
+			block = chain.GetBlockByHash(common.HexToHash(arg))
+		} else {
+			num, _ := strconv.Atoi(arg)
+			block = chain.GetBlockByNumber(uint64(num))
+		}
+		if block == nil {
+			fmt.Println("{}")
+			utils.Fatalf("block not found")
+		} else {
+			state, err := state.New(block.Root(), state.NewDatabase(chainDb))
+			if err != nil {
+				utils.Fatalf("could not create new state: %v", err)
+			}
+			fmt.Printf("%s\n", state.Dump())
+		}
+	}
+	chainDb.Close()
+	return nil
+}
+dump 子命令可以移除一个或多个特定的区块,先根据区块号获取区块,然后调用 state 的 Dump 移除即可 + +### geth/accountCommand.go +这部分主要是管理账户 +
func accountList(ctx *cli.Context) error {
+	stack, _ := makeConfigNode(ctx)
+	var index int
+	for _, wallet := range stack.AccountManager().Wallets() {
+		for _, account := range wallet.Accounts() {
+			fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
+			index++
+		}
+	}
+	return nil
+}
+拿到wallets中的所有账户 +
func accountCreate(ctx *cli.Context) error {
+	cfg := gethConfig{Node: defaultNodeConfig()}
+	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
+		if err := loadConfig(file, &cfg); err != nil {
+			utils.Fatalf("%v", err)
+		}
+	}
+	utils.SetNodeConfig(ctx, &cfg.Node)
+	scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
+	if err != nil {
+		utils.Fatalf("Failed to read configuration: %v", err)
+	}
+	password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
+	address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
+	if err != nil {
+		utils.Fatalf("Failed to create account: %v", err)
+	}
+	fmt.Printf("Address: {%x}\n", address)
+	return nil
+}
+创建一个账户,成功后输出地址 +
func accountUpdate(ctx *cli.Context) error {
+	if len(ctx.Args()) == 0 {
+		utils.Fatalf("No accounts specified to update")
+	}
+	stack, _ := makeConfigNode(ctx)
+	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
+	for _, addr := range ctx.Args() {
+		account, oldPassword := unlockAccount(ctx, ks, addr, 0, nil)
+		newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
+		if err := ks.Update(account, oldPassword, newPassword); err != nil {
+			utils.Fatalf("Could not update the account: %v", err)
+		}
+	}
+	return nil
+}
+先通过 AccountManager 拿到 keystore,然后调用 Update 更新密码 +
func accountImport(ctx *cli.Context) error {
+	keyfile := ctx.Args().First()
+	if len(keyfile) == 0 {
+		utils.Fatalf("keyfile must be given as argument")
+	}
+	key, err := crypto.LoadECDSA(keyfile)
+	if err != nil {
+		utils.Fatalf("Failed to load the private key: %v", err)
+	}
+	stack, _ := makeConfigNode(ctx)
+	passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
+	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
+	acct, err := ks.ImportECDSA(key, passphrase)
+	if err != nil {
+		utils.Fatalf("Could not create the account: %v", err)
+	}
+	fmt.Printf("Address: {%x}\n", acct.Address)
+	return nil
+}
+先通过 AccountManager 拿到 keystore,调用 ImportPreSaleKey 导入账户 + +### geth/consolecmd.go +
func localConsole(ctx *cli.Context) error {
+	node := makeFullNode(ctx)
+	startNode(ctx, node)
+	defer node.Stop()
+	client, err := node.Attach()
+	if err != nil {
+		utils.Fatalf("Failed to attach to the inproc geth: %v", err)
+	}
+	config := console.Config{
+		DataDir: utils.MakeDataDir(ctx),
+		DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
+		Client:  client,
+		Preload: utils.MakeConsolePreloads(ctx),
+	}
+	console, err := console.New(config)
+	if err != nil {
+		utils.Fatalf("Failed to start the JavaScript console: %v", err)
+	}
+	defer console.Stop(false)
+	if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
+		console.Evaluate(script)
+		return nil
+	}
+	console.Welcome()
+	console.Interactive()
+	return nil
+}
+启动本地的一个交互式 Javascript 环境,功能是通过 console 模块提供的,而 console 模块是对 robertkrimen/otto 的一个封装。otto 是一个 Golang 实现的 Javascript 解释器,可以实现在 Golang 中执行 Javascript,并且可以让在虚拟机里的 Javascript 调用 Golang 函数,实现 Golang 和 Javascript 的相互操作 +
func remoteConsole(ctx *cli.Context) error {
+	endpoint := ctx.Args().First()
+	if endpoint == "" {
+		path := node.DefaultDataDir()
+		if ctx.GlobalIsSet(utils.DataDirFlag.Name) {
+			path = ctx.GlobalString(utils.DataDirFlag.Name)
+		}
+		if path != "" {
+			if ctx.GlobalBool(utils.TestnetFlag.Name) {
+				path = filepath.Join(path, "testnet")
+			} else if ctx.GlobalBool(utils.RinkebyFlag.Name) {
+				path = filepath.Join(path, "rinkeby")
+			}
+		}
+		endpoint = fmt.Sprintf("%s/geth.ipc", path)
+	}
+	client, err := dialRPC(endpoint)
+	if err != nil {
+		utils.Fatalf("Unable to attach to remote geth: %v", err)
+	}
+	config := console.Config{
+		DataDir: utils.MakeDataDir(ctx),
+		DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
+		Client:  client,
+		Preload: utils.MakeConsolePreloads(ctx),
+	}
+	console, err := console.New(config)
+	if err != nil {
+		utils.Fatalf("Failed to start the JavaScript console: %v", err)
+	}
+	defer console.Stop(false)
+	if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
+		console.Evaluate(script)
+		return nil
+	}
+	console.Welcome()
+	console.Interactive()
+	return nil
+}
+启动一个 JS 交互式环境(连接到节点),通过指定 endpoint 的方式,连接到某个节点的交互式 Javascript 环境 +
func ephemeralConsole(ctx *cli.Context) error {
+	node := makeFullNode(ctx)
+	startNode(ctx, node)
+	defer node.Stop()
+	client, err := node.Attach()
+	if err != nil {
+		utils.Fatalf("Failed to attach to the inproc geth: %v", err)
+	}
+	config := console.Config{
+		DataDir: utils.MakeDataDir(ctx),
+		DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
+		Client:  client,
+		Preload: utils.MakeConsolePreloads(ctx),
+	}
+	console, err := console.New(config)
+	if err != nil {
+		utils.Fatalf("Failed to start the JavaScript console: %v", err)
+	}
+	defer console.Stop(false)
+	for _, file := range ctx.Args() {
+		if err = console.Execute(file); err != nil {
+			utils.Fatalf("Failed to execute %s: %v", file, err)
+		}
+	}
+	abort := make(chan os.Signal, 1)
+	signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		<-abort
+		os.Exit(0)
+	}()
+	console.Stop(true)
+	return nil
+}
+执行 Javascript 文件中的命令(可以为多个文件),通过遍历调用传输的文件路径,执行 console.Execute,执行 js 命令。 + +### geth/misccmd.go +
func makecache(ctx *cli.Context) error {
+	args := ctx.Args()
+	if len(args) != 2 {
+		utils.Fatalf(`Usage: geth makecache  `)
+	}
+	block, err := strconv.ParseUint(args[0], 0, 64)
+	if err != nil {
+		utils.Fatalf("Invalid block number: %v", err)
+	}
+	ethash.MakeCache(block, args[1])
+	return nil
+}
+生成 ethash 的验证缓存 +
func makedag(ctx *cli.Context) error {
+	args := ctx.Args()
+	if len(args) != 2 {
+		utils.Fatalf(`Usage: geth makedag  `)
+	}
+	block, err := strconv.ParseUint(args[0], 0, 64)
+	if err != nil {
+		utils.Fatalf("Invalid block number: %v", err)
+	}
+	ethash.MakeDataset(block, args[1])
+	return nil
+}
+通过调用 ethash 的 MakeDataset,生成挖矿需要的 DAG 数据集 +- versionCommand: 输出版本号 +- bugCommand: 给 https://github.com/ethereum/go-ethereum/issues/new 这个 url 拼接参数,给源代码仓库提一个 issue +- licenseCommand: 输出 License 信息 + +### geth/config.go +
func dumpConfig(ctx *cli.Context) error {
+	_, cfg := makeConfigNode(ctx)
+	comment := ""
+	if cfg.Eth.Genesis != nil {
+		cfg.Eth.Genesis = nil
+		comment += "# Note: this config doesn't contain the genesis block.\n\n"
+	}
+	out, err := tomlSettings.Marshal(&cfg)
+	if err != nil {
+		return err
+	}
+	io.WriteString(os.Stdout, comment)
+	os.Stdout.Write(out)
+	return nil
+}
+dumpConfig 函数通过 makeConfigNode 获取配置,然后将其输出在屏幕 diff --git a/consensus.md b/consensus.md new file mode 100644 index 0000000..e33a718 --- /dev/null +++ b/consensus.md @@ -0,0 +1,468 @@ +# Consensus + +clique 主要涉及POA,用于测试网络; ethash主要涉及POW,用于主网络; misc是用于之前DAO分叉的文件。 +下图是consensus文件中各组件的关系图: +![image](https://github.com/Billy1900/go-ethereum-code-analysis/blob/master/picture/Consensus-architecture.png) +Engine接口定义了共识引擎需要实现的所有函数,实际上按功能可以划分为2类: +- 区块验证类:以Verify开头,当收到新区块时,需要先验证区块的有效性 +- 区块盖章类:包括Prepare/Finalize/Seal等,用于最终生成有效区块(比如添加工作量证明) +与区块验证相关联的还有2个外部接口:Processor用于执行交易,而Validator用于验证区块内容和状态。另外,由于需要访问以前的区块链数据,抽象出了一个ChainReader接口,BlockChain和HeaderChain都实现了该接口以完成对数据的访问。 + +## 1.区块验证流程 +![image](https://github.com/Billy1900/go-ethereum-code-analysis/blob/master/picture/block-verification-process.png) +Downloader收到新区块后会调用BlockChain的InsertChain()函数插入新区块。在插入之前需要先要验证区块的有效性,基本分为4个步骤: +- 验证区块头:调用Ethash.VerifyHeaders() +- 验证区块内容:调用BlockValidator.VerifyBody()(内部还会调用Ethash.VerifyUncles()) +- 执行区块交易:调用BlockProcessor.Process()(基于其父块的世界状态) +- 验证状态转换:调用BlockValidator.ValidateState()
+如果验证成功,则往数据库中写入区块信息,然后广播ChainHeadEvent事件。 + +## 2.区块盖章流程 +![image](https://github.com/Billy1900/go-ethereum-code-analysis/blob/master/picture/block-seal-process.png) +新产生的区块必须经过“盖章(seal)”才能成为有效区块,具体到Ethash来说,就是要执行POW计算以获得低于设定难度的nonce值。这个其实在之前的挖矿流程分析中已经接触过了,主要分为3个步骤: +- 准备工作:调用Ethash.Prepare()计算难度值 +- 生成区块:调用Ethash.Finalize()打包新区块 +- 盖章:调用Ethash.Seal()进行POW计算,填充nonce值 + +## 3.实现分析 +### 3.1 consensus.go +该文件主要是定义整个consensus,chainReader是读取以前的区块数据,Engine是consensus工作的核心模块,POW是目前的一种机制,可以看到他的核心模块是Engine +
type PoW interface {
+	Engine
+	// Hashrate returns the current mining hashrate of a PoW consensus engine.
+	Hashrate() float64
+}
+### 3.2 ethan/algorithm.go +它涉及到挖矿算法的很多细节。 +
// cacheSize returns the size of the ethash verification cache that belongs to a certain
+// block number.
+func cacheSize(block uint64) uint64 {
+	epoch := int(block / epochLength)
+	if epoch < maxEpoch {
+		return cacheSizes[epoch]
+	}
+	return calcCacheSize(epoch)
+}
+
+// calcCacheSize calculates the cache size for epoch. The cache size grows linearly,
+// however, we always take the highest prime below the linearly growing threshold in order
+// to reduce the risk of accidental regularities leading to cyclic behavior.
+func calcCacheSize(epoch int) uint64 {
+	size := cacheInitBytes + cacheGrowthBytes*uint64(epoch) - hashBytes
+	for !new(big.Int).SetUint64(size / hashBytes).ProbablyPrime(1) { // Always accurate for n < 2^64
+		size -= 2 * hashBytes
+	}
+	return size
+}
+cache的具体作用涉及到挖矿计算的细节,如下: +Ethash 是以太坊使用的 PoW 算法,其原理可以用一个公式来概括:
+**RAND(h,n)<=M/d**
+其中 h 是区块头的哈希值(没有 Nonce),n 是 Nonce 值,M 是一个极大的数字,d 指挖矿难度,RAND 是一个根据参数生成随机值的操作,挖矿的过程简单来说就是寻找适合的 nonce,使上述不等式成立。原理和比特币的基本相同,但 Ethash 稍特别一点,因为 geth 的开发者在设计初期就考虑了抵制矿机的问题里 +
Ethash 的具体步骤为: +- 对于每个区块,先算出一个种子。种子的计算只依赖当前区块信息。 +- 使用种子生成伪随机数据集,称为 cache。轻客户端需要保存 cache +- 基于 cache 生成 1GB 大小的数据集,称为 the DAG。这个数据集的每一个元素都依赖于 cache 中的某几个元素,只要有 cache 就可以快速计算出 DAG 中指定位置的元素。完整可挖矿客户端需要保存 DAG。 +- 挖矿可以概括为从 DAG 中随机选择元素,然后暴力枚举选择一个 nonce 值,对其进行哈希计算,使其符合约定的难度,而这个难度其实就是要求哈希值的前缀包括多少个0。验证的时候,基于 cache 计算指定位置 DAG 元素,然后验证这个元素集合的哈希值结果小于某个值,这个过程只需要普通 CPU 和普通内存。 +- cache 和 DAG 每过一个周期更新一次,一个周期长度是 30000 个区块。DAG 只取决于区块数量,大小会随着时间推移线性增长,从 1GB 开始,每年大约增加 7GB。由于 DAG 需要很长时间生成,所以 geth 每次会维护2个 DAG 集合。 +
// datasetSize returns the size of the ethash mining dataset that belongs to a certain
+// block number.
+func datasetSize(block uint64) uint64 {
+	epoch := int(block / epochLength)
+	if epoch < maxEpoch {
+		return datasetSizes[epoch]
+	}
+	return calcDatasetSize(epoch)
+}
+
+// calcDatasetSize calculates the dataset size for epoch. The dataset size grows linearly,
+// however, we always take the highest prime below the linearly growing threshold in order
+// to reduce the risk of accidental regularities leading to cyclic behavior.
+func calcDatasetSize(epoch int) uint64 {
+	size := datasetInitBytes + datasetGrowthBytes*uint64(epoch) - mixBytes
+	for !new(big.Int).SetUint64(size / mixBytes).ProbablyPrime(1) { // Always accurate for n < 2^64
+		size -= 2 * mixBytes
+	}
+	return size
+}
+dataset就是上文中提到的数据集。datasetsize和cachesetsize都已经硬编码写进了文件当中, +
// hasher is a repetitive hasher allowing the same hash data structures to be
+// reused between hash runs instead of requiring new ones to be created.
+type hasher func(dest []byte, data []byte)
+
+// makeHasher creates a repetitive hasher, allowing the same hash data structures to
+// be reused between hash runs instead of requiring new ones to be created. The returned
+// function is not thread safe!
+func makeHasher(h hash.Hash) hasher {
+	// sha3.state supports Read to get the sum, use it to avoid the overhead of Sum.
+	// Read alters the state but we reset the hash before every operation.
+	type readerHash interface {
+		hash.Hash
+		Read([]byte) (int, error)
+	}
+	rh, ok := h.(readerHash)
+	if !ok {
+		panic("can't find Read method on hash")
+	}
+	outputLen := rh.Size()
+	return func(dest []byte, data []byte) {
+		rh.Reset()
+		rh.Write(data)
+		rh.Read(dest[:outputLen])
+	}
+}
+
+// seedHash is the seed to use for generating a verification cache and the mining
+// dataset.
+func seedHash(block uint64) []byte {
+	seed := make([]byte, 32)
+	if block < epochLength {
+		return seed
+	}
+	keccak256 := makeHasher(sha3.NewLegacyKeccak256())
+	for i := 0; i < int(block/epochLength); i++ {
+		keccak256(seed, seed)
+	}
+	return seed
+}
+seedHash也就是挖矿的第一步生成种子,makeHasher也就是生成种子(hash的过程) +
func generateCache(dest []uint32, epoch uint64, seed []byte) 
+generateCache是指从之前的种子中根据规则生成cache. The cache production process involves first sequentially filling up 32 MB of memory, then performing two passes of Sergio Demian Lerner's RandMemoHash algorithm from Strict Memory Hard Hashing Functions (2014). The output is a set of 524288 64-byte values. +
func generateDatasetItem(cache []uint32, index uint32, keccak512 hasher) []byte
+func generateDataset(dest []uint32, epoch uint64, cache []uint32) 
+generateDatasetItem combines data from 256 pseudorandomly selected cache nodes, and hashes that to compute a single dataset node. generateDataset generates the entire ethash dataset for mining. +
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte)
+func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte)
+func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte)
+
+- hashimoto aggregates data from the full dataset in order to produce our final value for a particular header hash and nonce. +- hashimotoLight aggregates data from the full dataset (using only a small in-memory cache) in order to produce our final value for a particular header hash and nonce. +- hashimotoFull aggregates data from the full dataset (using the full in-memory dataset) in order to produce our final value for a particular header hash and nonce. +### 3.3 ethan/api.go +the purpose is that API exposes ethash related methods for the RPC interface. +
func (api *API) GetWork() ([4]string, error)
+GetWork returns a work package for external miner. The work package consists of 3 strings: +- result[0] - 32 bytes hex encoded current block header pow-hash +- result[1] - 32 bytes hex encoded seed hash used for DAG +- result[2] - 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty +- result[3] - hex encoded block number +
func (api *API) SubmitWork(nonce types.BlockNonce, hash, digest common.Hash) bool 
+SubmitWork can be used by external miner to submit their POW solution. It returns an indication if the work was accepted. Note either an invalid solution, a stale work a non-existent work will return false. +
func (api *API) SubmitHashRate(rate hexutil.Uint64, id common.Hash) bool
+SubmitHashrate can be used for remote miners to submit their hash rate. This enables the node to report the combined hash rate of all miners which submit work through this node.[what is hash rate?](https://www.buybitcoinworldwide.com/mining/hash-rate/), simply it can be regared as computation. +### 3.4 ethan/consensus.go +ethan/consensus.go实现的大多函数是对consensus/onsensus.go中Engine中的interface的函数具体实现.具体功能注释都已经写的很详尽,在此不过多赘述。故只挑了一些进行注释。 +#### ethan/consensus.go/VerifyHeaders() +VerifyHeaders和VerifyHeader实现原理都差不多,只不过VerifyHeaders是处理一堆headers +
// Spawn as many workers as allowed threads
+    workers := runtime.GOMAXPROCS(0)
+    if len(headers) < workers {
+        workers = len(headers)
+    }
+首先根据待验证区块的个数确定需要创建的线程数,最大不超过CPU个数。 +
var (
+        inputs = make(chan int)
+        done   = make(chan int, workers)
+        errors = make([]error, len(headers))
+        abort  = make(chan struct{})
+    )
+    for i := 0; i < workers; i++ {
+        go func() {
+            for index := range inputs {
+                errors[index] = ethash.verifyHeaderWorker(chain, headers, seals, index)
+                done <- index
+            }
+        }()
+    }
+这一步就是创建线程了,每个线程会从inputs信道中获得待验证区块的索引号,然后调用verifyHeaderWorker()函数验证该区块,验证完后向done信道发送区块索引号。 +
errorsOut := make(chan error, len(headers))
+    go func() {
+        defer close(inputs)
+        var (
+            in, out = 0, 0
+            checked = make([]bool, len(headers))
+            inputs  = inputs
+        )
+        for {
+            select {
+            case inputs <- in:
+                if in++; in == len(headers) {
+                    // Reached end of headers. Stop sending to workers.
+                    inputs = nil
+                }
+            case index := <-done:
+                for checked[index] = true; checked[out]; out++ {
+                    errorsOut <- errors[out]
+                    if out == len(headers)-1 {
+                        return
+                    }
+                }
+            case <-abort:
+                return
+            }
+        }
+    }()
+    return abort, errorsOut
+这一步启动一个循环,首先往inputs信道中依次发送区块索引号,然后再从done信道中依次接收子线程处理完成的事件,最后返回验证结果。 +接下来我们就分析一下ethash.verifyHeaderWorker()主要做了哪些工作: +
func (ethash *Ethash) verifyHeaderWorker(chain consensus.ChainReader, headers []*types.Header, seals []bool, index int) error {
+    var parent *types.Header
+    if index == 0 {
+        parent = chain.GetHeader(headers[0].ParentHash, headers[0].Number.Uint64()-1)
+    } else if headers[index-1].Hash() == headers[index].ParentHash {
+        parent = headers[index-1]
+    }
+    if parent == nil {
+        return consensus.ErrUnknownAncestor
+    }
+    if chain.GetHeader(headers[index].Hash(), headers[index].Number.Uint64()) != nil {
+        return nil // known block
+    }
+    return ethash.verifyHeader(chain, headers[index], parent, false, seals[index])
+}
+首先通过ChainReader拿到父块的header,然后调用ethash.verifyHeader(),这个函数就是真正去验证区块头了,这个函数比较长,大概列一下有哪些检查项: +- 时间戳超前当前时间不得大于15s +- 时间戳必须大于父块时间戳 +- 通过父块计算出的难度值必须和区块头难度值相同 +- 消耗的gas必须小于gas limit +- 当前gas limit和父块gas limit的差值必须在规定范围内 +- 区块高度必须是父块高度+1 +- 调用ethash.VerifySeal()检查工作量证明 +- 验证硬分叉相关的数据 +- ethash.VerifySeal()函数,这个函数主要是用来检查工作量证明,用于校验难度的有效性nonce是否小于目标值(解题成功) +> verifyHeader +>- 校验extra大小 +>- 校验区块时间戳,跟当前时间比 +>- 校验难度值 +>- 校验gaslimit上线 +>- 校验区块的总gasuserd小于 gaslimit +>- 校验区块的gaslimit 是在合理范围 +>- 特殊的校验,比如dao分叉后的几个块extra里面写了特殊数据,来判断一下 + +#### ethan/consensus.go/VerifyUncles() +这个函数是在BlockValidator.VerifyBody()内部调用的,主要是验证叔块的有效性。 +
    if len(block.Uncles()) > maxUncles {
+        return errTooManyUncles
+    }
+以太坊规定每个区块打包的叔块不能超过2个。
+    uncles, ancestors := set.New(), make(map[common.Hash]*types.Header)
+    number, parent := block.NumberU64()-1, block.ParentHash()
+    for i := 0; i < 7; i++ {
+        ancestor := chain.GetBlock(parent, number)
+        if ancestor == nil {
+            break
+        }
+        ancestors[ancestor.Hash()] = ancestor.Header()
+        for _, uncle := range ancestor.Uncles() {
+            uncles.Add(uncle.Hash())
+        }
+        parent, number = ancestor.ParentHash(), number-1
+    }
+    ancestors[block.Hash()] = block.Header()
+    uncles.Add(block.Hash())
+这段代码收集了当前块前7层的祖先块和叔块,用于后面的验证。 +
    for _, uncle := range block.Uncles() {
+        // Make sure every uncle is rewarded only once
+        hash := uncle.Hash()
+        if uncles.Has(hash) {
+            return errDuplicateUncle
+        }
+        uncles.Add(hash)
+        // Make sure the uncle has a valid ancestry
+        if ancestors[hash] != nil {
+            return errUncleIsAncestor
+        }
+        if ancestors[uncle.ParentHash] == nil || uncle.ParentHash == block.ParentHash() {
+            return errDanglingUncle
+        }
+        if err := ethash.verifyHeader(chain, uncle, ancestors[uncle.ParentHash], true, true); err != nil {
+            return err
+        }
+    }
+ 遍历当前块包含的叔块,做以下检查: +- 如果祖先块中已经包含过了该叔块,返回错误 +- 如果发现该叔块其实是一个祖先块(即在主链上),返回错误 +- 如果叔块的父块不在这7层祖先中,返回错误 +- 如果叔块和当前块拥有共同的父块,返回错误(也就是说不能打包和当前块相同高度的叔块) +- 最后验证一下叔块头的有效性 + +#### ethan/consensus.go/Prepare() +

+func (ethash *Ethash) Prepare(chain consensus.ChainReader, header *types.Header) error {
+    parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
+    if parent == nil {
+        return consensus.ErrUnknownAncestor
+    }
+    header.Difficulty = ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)
+    return nil
+}
+可以看到,会调用CalcDifficulty()计算难度值,继续跟踪: +
func (ethash *Ethash) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
+    return CalcDifficulty(chain.Config(), time, parent)
+}
+
+func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
+    next := new(big.Int).Add(parent.Number, big1)
+    switch {
+    case config.IsByzantium(next):
+        return calcDifficultyByzantium(time, parent)
+    case config.IsHomestead(next):
+        return calcDifficultyHomestead(time, parent)
+    default:
+        return calcDifficultyFrontier(time, parent)
+    }
+}
+根据以太坊的Roadmap,会经历Frontier,Homestead,Metropolis,Serenity这几个大的版本,当前处于Metropolis阶段。Metropolis又分为2个小版本:Byzantium和Constantinople,目前的最新代码版本是Byzantium,因此会调用calcDifficultyByzantium()函数。
+计算难度的公式如下:
+diff = (parent_diff +(parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))) + 2^(periodCount - 2)
+>- parent_diff :上一个区块的难度 +>- block_timestamp :当前块的时间戳 +>- parent_timestamp:上一个块的时间戳 +>- periodCount :区块num/100000 +>- block_timestamp - parent_timestamp 差值小于10秒 变难
+ block_timestamp - parent_timestamp 差值10-20秒 不变
+ block_timestamp - parent_timestamp 差值大于20秒 变容易,并且大的越多,越容易,但是又上限 +>- 总体上块的难度是递增的 +>- seal 开始做挖矿的事情,“解题”直到成功或者退出.根据挖矿难度计算目标值,选取随机数nonce+区块头(不包含nonce)的hash,再做一次hash,结果小于目标值,则退出,否则循环重试.如果外部退出了(比如已经收到这个块了),则立马放弃当前块的打包.Finalize() 做挖矿成功后最后善后的事情,计算矿工的奖励:区块奖励,叔块奖励, + +前面一项是根据父块难度值继续难度调整,而后面一项就是传说中的“难度炸弹”。关于难度炸弹相关的具体细节可以参考下面这篇文章: +https://juejin.im/post/59ad6606f265da246f382b88
+由于PoS共识机制开发进度延迟,不得不减小难度炸弹从而延迟“冰川时代”的到来,具体做法就是把当前区块高度减小3000000,参见以下代码: +
   // calculate a fake block number for the ice-age delay:
+    //   https://github.com/ethereum/EIPs/pull/669
+    //   fake_block_number = min(0, block.number - 3_000_000
+    fakeBlockNumber := new(big.Int)
+    if parent.Number.Cmp(big2999999) >= 0 {
+        fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, big2999999) // Note, parent is 1 less than the actual block number
+    }
+ + #### ethash/consensus.go/FinalizeAndAssemble() +
func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) {
+    // Accumulate any block and uncle rewards and commit the final state root
+    accumulateRewards(chain.Config(), state, header, uncles)
+    header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
+    // Header seems complete, assemble into a block and return
+    return types.NewBlock(header, txs, uncles, receipts), nil
+}
+这个挖矿流程是先计算收益,然后生成MPT的Merkle Root,最后创建新区块。 + +#### ethash/consensus.go/sealer/seal() +这个函数就是真正执行POW计算的地方了,代码位于consensus/ethash/sealer.go。代码比较长,分段进行分析: +
    abort := make(chan struct{})
+    found := make(chan *types.Block)
+首先创建了两个channel,用于退出和发现nonce时发送事件。 +
    ethash.lock.Lock()
+    threads := ethash.threads
+    if ethash.rand == nil {
+        seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
+        if err != nil {
+            ethash.lock.Unlock()
+            return nil, err
+        }
+        ethash.rand = rand.New(rand.NewSource(seed.Int64()))
+    }
+    ethash.lock.Unlock()
+    if threads == 0 {
+        threads = runtime.NumCPU()
+    }
+接着初始化随机数种子和线程数 +
    var pend sync.WaitGroup
+    for i := 0; i < threads; i++ {
+        pend.Add(1)
+        go func(id int, nonce uint64) {
+            defer pend.Done()
+            ethash.mine(block, id, nonce, abort, found)
+        }(i, uint64(ethash.rand.Int63()))
+    }
+然后就是创建线程进行挖矿了,会调用ethash.mine()函数。 +
    // Wait until sealing is terminated or a nonce is found
+    var result *types.Block
+    select {
+    case <-stop:
+        // Outside abort, stop all miner threads
+        close(abort)
+    case result = <-found:
+        // One of the threads found a block, abort all others
+        close(abort)
+    case <-ethash.update:
+        // Thread count was changed on user request, restart
+        close(abort)
+        pend.Wait()
+        return ethash.Seal(chain, block, stop)
+    }
+    // Wait for all miners to terminate and return the block
+    pend.Wait()
+    return result, nil
+最后就是等待挖矿结果了,有可能找到nonce挖矿成功,也有可能别人先挖出了区块从而需要终止挖矿。 +
ethash.mine()函数的实现,先看一些变量声明: +
    var (
+        header  = block.Header()
+        hash    = header.HashNoNonce().Bytes()
+        target  = new(big.Int).Div(maxUint256, header.Difficulty)
+        number  = header.Number.Uint64()
+        dataset = ethash.dataset(number)
+    )
+    // Start generating random nonces until we abort or find a good one
+    var (
+        attempts = int64(0)
+        nonce    = seed
+    )
+其中hash指的是不带nonce的区块头hash值,nonce是一个随机数种子。target是目标值,等于2^256除以难度值,我们接下来要计算的hash值必须小于这个目标值才算挖矿成功。接下来就是不断修改nonce并计算hash值了: +
            digest, result := hashimotoFull(dataset.dataset, hash, nonce)
+     if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
+     // Correct nonce found, create a new header with it
+     header = types.CopyHeader(header)
+     header.Nonce = types.EncodeNonce(nonce)
+     header.MixDigest = common.BytesToHash(digest)
+     // Seal and return a block (if still needed)
+     select {
+     case found <- block.WithSeal(header):
+     logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
+     case <-abort:
+                logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
+                }
+                break search
+            }
+     nonce++
+hashimotoFull()函数内部会把hash和nonce拼在一起,计算出一个摘要(digest)和一个hash值(result)。如果hash值满足难度要求,挖矿成功,填充区块头的Nonce和MixDigest字段,然后调用block.WithSeal()生成盖过章的区块: +
func (b *Block) WithSeal(header *Header) *Block {
+    cpy := *header
+    return &Block{
+        header:       &cpy,
+        transactions: b.transactions,
+        uncles:       b.uncles,
+    }
+}
+### 3.5 ethan/ethash.go +将此文件内容分为几个大块进行理解
+1)memoryMap块
+memoryMapFile tries to memory map an already opened file descriptor, memoryMap tries to memory map a file of uint32s for read only access, memoryMapAndGenerate tries to memory map a temporary file of uint32s for write access, fill it with the data from a generator and then move it into the final path requested. +
func memoryMap(path string) (*os.File, mmap.MMap, []uint32, error)
+func memoryMapFile(file *os.File, write bool) (mmap.MMap, []uint32, error)
+func memoryMapAndGenerate(path string, size uint64, generator func(buffer []uint32)) (*os.File, mmap.MMap, []uint32, error)
+2)Lru块
+Lru是一个cache的存储策略,此处主要是用于优化dataset和cache中的存储数据方式
+3)cache块
+具体可以见文件中的注释,具体作用前面也已经说清楚,主要是cache的具体逻辑实现
+4)dataset块
+具体可以见文件中的注释,具体作用前面也已经说清楚,主要是函数的具体逻辑实现
+5)config块
+
// Config are the configuration parameters of the ethash.
+type Config struct {
+	CacheDir       string
+	CachesInMem    int
+	CachesOnDisk   int
+	DatasetDir     string
+	DatasetsInMem  int
+	DatasetsOnDisk int
+	PowMode        Mode
+}
+6)后续代码块
+后续代码多为挖矿所需定义的结构体,看注释就可以解决疑惑。 +### 3.6 ethan/sealer.go +sealer主要是用于最终为block打标签,也就是最终的挖矿计算的过程。主要的函数如下: +
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error 
+- Seal implements consensus.Engine, attempting to find a nonce that satisfies the block's difficulty requirements. +
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) 
+- mine is the actual proof-of-work miner that searches for a nonce starting from seed that results in correct final block difficulty. +
func (ethash *Ethash) remote(notify []string, noverify bool)
+- remote is a standalone goroutine to handle remote mining related stuff. diff --git "a/core-blockchain\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/core-blockchain\346\272\220\347\240\201\345\210\206\346\236\220.md" index 4594510..07e202a 100644 --- "a/core-blockchain\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/core-blockchain\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -1,3 +1,4 @@ +# core/blockchain.go 从测试案例来看,blockchain的主要功能点有下面几点. 1. import. @@ -9,15 +10,15 @@ 7. 支持Fast importing. 8. Light vs Fast vs Full processing 在处理区块头上面的效果相等. -可以看到blockchain的主要功能是维护区块链的状态, 包括区块的验证,插入和状态查询. +blockchain的主要功能是维护区块链的状态, 包括区块的验证,插入和状态查询. -名词解释: +>名词解释: -什么是规范的区块链 +>什么是规范的区块链 -因为在区块的创建过程中,可能在短时间内产生一些分叉, 在我们的数据库里面记录的其实是一颗区块树.我们会认为其中总难度最高的一条路径认为是我们的规范的区块链. 这样有很多区块虽然也能形成区块链,但是不是规范的区块链. +>因为在区块的创建过程中,可能在短时间内产生一些分叉, 在我们的数据库里面记录的其实是一颗区块树.我们会认为其中总难度最高的一条路径认为是我们的规范的区块链. 这样有很多区块虽然也能形成区块链,但是不是规范的区块链. -数据库结构: +**数据库结构:** 区块的hash值和区块头的hash值是同样的么。所谓的区块的Hash值其实就是Header的区块值。 // key -> value @@ -57,6 +58,7 @@ key | value|说明|插入|删除| 数据结构 +Blockchian structure: // BlockChain represents the canonical chain given a database with a genesis // block. The Blockchain manages chain imports, reverts, chain reorganisations. @@ -179,6 +181,8 @@ key | value|说明|插入|删除| go bc.update() return bc, nil } + +BlockChain 的初始化需要 ethdb.Database, *CacheConfig, params.ChainConfig, consensus.Engine,vm.Config 参数。它们分别表示 db 对象;缓存配置(在 core/blockchain.go 中定义);区块链配置(可通过 core/genesis.go 中的 SetupGenesisBlock 拿到);一致性引擎(可通过 core/blockchain.go 中的 CreateConsensusEngine 得到);虚拟机配置(通过 core/vm 定义)这些实参需要提前定义.回到 NewBlockChain 的具体代码,首先判断是否有默认 cacheConfig,如果没有根据默认配置创建 cacheConfig,再通过 hashicorp 公司的 lru 模块创建 bodyCache, bodyRLPCache 等缓存对象(lru 是 last recently used 的缩写,常见数据结构,不了解的朋友请自行查阅相关资料),根据这些信息创建 BlockChain 对象,然后通过调用 BlockChain 的 SetValidator 和 SetProcessor 方法创建验证器和处理器,接下来通过 NewHeaderChain 获得区块头,尝试判断创始区块是否存在,bc.loadLastState() 加载区块最新状态,最后检查当前状态,确保本地运行的区块链上没有非法的区块。 loadLastState, 加载数据库里面的最新的我们知道的区块链状态. 这个方法假设已经获取到锁了. @@ -242,7 +246,9 @@ loadLastState, 加载数据库里面的最新的我们知道的区块链状态. return nil } -goroutine update的处理非常简单. 定时处理future blocks. +loadLastState 会从数据库中加载区块链状态,首先通过 GetHeadBlockHash 从数据库中取得当前区块头,如果当前区块不存在,即数据库为空的话,通过 Reset 将创始区块写入数据库以达到重置目的。如果当前区块不存在,同样通过 Reset 重置。接下来确认当前区块的世界状态是否正确;如果有问题,则通过 repair 进行修复,repair 中是一个死循环,它会一直回溯当前区块,直到找到对应的世界状态。然后通过 bc.hc.SetCurrentHeader 设置当前区块头,并恢复快速同步区块。 + +update() 的作用是定时处理 Future 区块,简单地来说就是定时调用 procFutureBlocks。procFutureBlocks 可以从 futureBlocks 拿到需要插入的区块,最终会调用 InsertChain 将区块插入到区块链中。 func (bc *BlockChain) update() { futureTimer := time.Tick(5 * time.Second) @@ -347,8 +353,8 @@ SetHead将本地链回卷到新的头部。 在给定新header之上的所有内 return bc.loadLastState() } -InsertChain,插入区块链, 插入区块链尝试把给定的区块插入到规范的链条,或者是创建一个分叉. 如果发生错误,那么会返回错误发生时候的index和具体的错误信息. - +InsertChain 将尝试将给定的区块插入到规范的区块链中,或者创建一个分支,插入后,会通过 PostChainEvents 触发所有事件 + // InsertChain attempts to insert the given batch of blocks in to the canonical // chain or, otherwise, create a fork. If an error is returned it will return // the index number of the failing block as well an error describing what went @@ -519,6 +525,7 @@ insertChain方法会执行区块链插入,并收集事件信息. 因为需要使 } return 0, events, coalescedLogs, nil } +首先做一个健康检查,确保要插入的链是有序且相互连接的。接下来通过 bc.engine.VerifyHeaders 调用一致性引擎来验证区块头是有效的。进入 for i, block := range chain 循环后,接收 results 这个 chan,可以获得一致性引擎获得区块头的结果,如果是已经插入的区块,跳过;如果是未来的区块,时间距离不是很长,加入到 futureBlocks 中,否则返回一条错误信息;如果没能找到该区块祖先,但在 futureBlocks 能找到,也加入到 futureBlocks 中。加入 futureBlocks 的过程结束后,通过 core/state_processor.go 中的 Process 改变世界状态. 在返回收据,日志,使用的 Gas 后。通过 bc.Validator().ValidateState 再次验证,通过后,通过 WriteBlockAndState 写入区块以及相关状态到区块链。最后,如果生成了一个新的区块头,最新的区块头等于 lastCanon 的哈希值,发布一个 ChainHeadEvent 的事件。 WriteBlockAndState,把区块写入区块链. @@ -600,6 +607,7 @@ WriteBlockAndState,把区块写入区块链. bc.futureBlocks.Remove(block.Hash()) return status, nil } +WriteBlockWithState 将区块以及相关所有的状态写入数据库。首先通过 bc.GetTd(block.ParentHash(), block.NumberU64()-1) 获取待插入区块的总难度,bc.GetTd(bc.currentBlock.Hash(), bc.currentBlock.NumberU64()) 计算当前区块的区块链的总难度,externTd := new(big.Int).Add(block.Difficulty(), ptd) 获得新的区块链的总难度。通过 bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd) 写入区块 hash,高度,对应总难度。然后使用 batch 的方式写入区块的其他数据。插入数据后,判断这个区块的父区块是否为当前区块,如果不是,说明存在分叉,调用 reorg 重新组织区块链。插入成功后,调用 bc.futureBlocks.Remove(block.Hash()) 从 futureBlocks 中移除区块。 reorgs方法是在新的链的总难度大于本地链的总难度的情况下,需要用新的区块链来替换本地的区块链为规范链。 @@ -722,4 +730,4 @@ reorgs方法是在新的链的总难度大于本地链的总难度的情况下 return nil } - +reorg 方法用来将新区块链替换本地区块链为规范链。对于老链比新链高的情况,减少老链,让它和新链一样高;否则的话减少新链,待后续插入。潜在的会丢失的交易会被当做事件发布。接着进入一个 for 循环,找到两条链共同的祖先。再将上述减少新链阶段保存的 newChain 一块块插入到链中,更新规范区块链的 key,并且写入交易的查询信息。最后是清理工作,删除交易查询信息,删除日志,并通过 bc.rmLogsFeed.Send 发送消息通知,删除了哪些旧链则通过 bc.chainSideFeed.Send 进行消息通知。 diff --git "a/core-genesis\345\210\233\344\270\226\345\214\272\345\235\227\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/core-genesis\345\210\233\344\270\226\345\214\272\345\235\227\346\272\220\347\240\201\345\210\206\346\236\220.md" index 67d6b3b..a46e0ae 100644 --- "a/core-genesis\345\210\233\344\270\226\345\214\272\345\235\227\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/core-genesis\345\210\233\344\270\226\345\214\272\345\235\227\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -1,3 +1,5 @@ +# core/genesis.go + genesis 是创世区块的意思. 一个区块链就是从同一个创世区块开始,通过规则形成的.不同的网络有不同的创世区块, 主网络和测试网路的创世区块是不同的. 这个模块根据传入的genesis的初始值和database,来设置genesis的状态,如果不存在创世区块,那么在database里面创建它。 @@ -29,6 +31,16 @@ genesis 是创世区块的意思. 一个区块链就是从同一个创世区块 // GenesisAlloc 指定了最开始的区块的初始状态. type GenesisAlloc map[common.Address]GenesisAccount +genesisaccount, +
type GenesisAlloc map[common.Address]GenesisAccount
+type GenesisAccount struct {
+	Code       []byte                      `json:"code,omitempty"`
+	Storage    map[common.Hash]common.Hash `json:"storage,omitempty"`
+	Balance    *big.Int                    `json:"balance" gencodec:"required"`
+	Nonce      uint64                      `json:"nonce,omitempty"`
+	PrivateKey []byte                      `json:"secretKey,omitempty"`
+}
+ SetupGenesisBlock, @@ -115,9 +127,9 @@ SetupGenesisBlock, // 如果是主网络会从这里退出。 return newcfg, stored, WriteChainConfig(db, stored, newcfg) } +SetupGenesisBlock 会根据创世区块返回一个区块链的配置。从 db 参数中拿到的区块里如果没有创世区块的话,首先提交一个新区块。接着通过调用 genesis.configOrDefault(stored) 拿到当前链的配置,测试兼容性后将配置写回 DB 中。最后返回区块链的配置信息。 - -ToBlock, 这个方法使用genesis的数据,使用基于内存的数据库,然后创建了一个block并返回。 +ToBlock, 这个方法使用genesis的数据,使用基于内存的数据库,然后创建了一个block并返回(通过 types.NewBlock) // ToBlock creates the block and state of a genesis specification. diff --git a/picture/Consensus-architecture.png b/picture/Consensus-architecture.png new file mode 100644 index 0000000..906ea1d Binary files /dev/null and b/picture/Consensus-architecture.png differ diff --git a/picture/accounts.png b/picture/accounts.png new file mode 100644 index 0000000..dc28b96 Binary files /dev/null and b/picture/accounts.png differ diff --git a/picture/block-seal-process.png b/picture/block-seal-process.png new file mode 100644 index 0000000..296fecd Binary files /dev/null and b/picture/block-seal-process.png differ diff --git a/picture/block-verification-process.png b/picture/block-verification-process.png new file mode 100644 index 0000000..0c102c0 Binary files /dev/null and b/picture/block-verification-process.png differ diff --git a/types.md b/types.md new file mode 100644 index 0000000..b9fa249 --- /dev/null +++ b/types.md @@ -0,0 +1,83 @@ +# core/types +### core/types/block.go +block data stucture: +
type Block struct {
+	header       *Header
+	uncles       []*Header
+	transactions Transactions
+	hash atomic.Value
+	size atomic.Value
+	td *big.Int
+	ReceivedAt   time.Time
+	ReceivedFrom interface{}
+}
+|字段 |描述| +|--------|------------------------------| +|header |指向 Header 结构(之后会详细说明),header 存储一个区块的基本信息。| +|uncles |指向 Header 结构| +|transactions| 一组 transaction 结构| +|hash |当前区块的哈希值| +|size |当前区块的大小| +|td |当前区块高度| +|ReceivedAt| 接收时间| +|ReceivedFrom| 来源| + +交易组成区块,一个一个区块以单向链表的形式连在一起组成区块链 +
Header data structure: +
type Header struct {
+	ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
+	UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
+	Coinbase    common.Address `json:"miner"            gencodec:"required"`
+	Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
+	TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
+	ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
+	Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
+	Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
+	Number      *big.Int       `json:"number"           gencodec:"required"`
+	GasLimit    uint64         `json:"gasLimit"         gencodec:"required"`
+	GasUsed     uint64         `json:"gasUsed"          gencodec:"required"`
+	Time        *big.Int       `json:"timestamp"        gencodec:"required"`
+	Extra       []byte         `json:"extraData"        gencodec:"required"`
+	MixDigest   common.Hash    `json:"mixHash"          gencodec:"required"`
+	Nonce       BlockNonce     `json:"nonce"            gencodec:"required"`
+}
+|字段| 描述| +|--------|-------| +|ParentHash| 父区块的哈希值| +|UncleHash |叔区块的哈希值| +|Coinbase |矿工得到奖励的账户,一般是矿工本地第一个账户| +|Root |表示当前所有用户状态| +|TxHash |本区块所有交易 Hash,即摘要| +|ReceiptHash |本区块所有收据 Hash,即摘要| +|Bloom |布隆过滤器,用来搜索收据| +|Difficulty |该区块难度,动态调整,与父区块和本区块挖矿时间有关。 +|Number |该区块高度| +|GasLimit gas |用量上限,该数值根据父区块 gas 用量调节,如果 parentGasUsed > parentGasLimit * (2/3) ,则增大该数值,反之则减小该数值。| +|GasUsed |实际花费的 gas| +|Time |新区块的出块时间,严格来说是开始挖矿的时间| +|Extra |额外数据| +|MixDigest| 混合哈希,与nonce 结合使用| +|Nonce |加密学中的概念| +ParentHash 表示该区块的父区块哈希,我们通过 ParentHash 这个字段将一个一个区块连接起来组成区块链,但实际上我们并不会直接将链整个的存起来,它是以一定的数据结构一块一块存放的,geth 的底层数据库用的是 LevelDB,这是一个 key-value 数据库,要得到父区块时,我们通过 ParentHash 以及其他字符串组成 key,在 LevelDB 中查询该 key 对应的值,就能拿到父区块。 + +### core/types/transaction.go +
type Transaction struct {
+	data txdata
+	// caches
+	hash atomic.Value
+	size atomic.Value
+	from atomic.Value
+}
+type txdata struct {
+	AccountNonce uint64          `json:"nonce"    gencodec:"required"`
+	Price        *big.Int        `json:"gasPrice" gencodec:"required"`
+	GasLimit     uint64          `json:"gas"      gencodec:"required"`
+	Recipient    *common.Address `json:"to"       rlp:"nil"`
+	Amount       *big.Int        `json:"value"    gencodec:"required"`
+	Payload      []byte          `json:"input"    gencodec:"required"`
+	V *big.Int `json:"v" gencodec:"required"`
+	R *big.Int `json:"r" gencodec:"required"`
+	S *big.Int `json:"s" gencodec:"required"`
+	Hash *common.Hash `json:"hash" rlp:"-"`
+}
+转账的定义中只有转入方,转出方的地址没有直接暴露。每一笔转账都有独立的 Price 和 GasLimit,这是 Ethereum 的安全保护策略