初链主网Beta版于新加坡时间2018年09月28日08:00正式上线,在此之前,07:56分PBFT委员会第一次共识出块和TrueChain fPOW创世区块被挖出,而作为true 社区的一员,我也参与了这次初链主网Beta版的测试挖矿。目前true 主网的beta 版在2018年10月10日第一期的测试已经停止,第二期的创世区块预计在10月15日启动,并且恢复Beta版主网的全网运行。由于主网beta版已经停止,因此我将基于true 主网beta版本的代码搭建私链,并且进行解释,true 私链的搭建大家可以参考true 社区成员写的True链开发实战 进行自行搭建。
True 创建创世区块:命令:getrue --datadir Test init path/to/cmd/getrue/genesis.json
此命令主要作用为 生成创世区块,生成帐号的一些信息。Test文件夹内存放区块信息和账号信息,日志信息,genesis.json则为初始化的一些配置参数。
True -Beta 私链启动
True 启动测试链命令:getrue --datadir Test --nodiscover console
启动命令执行,我们要找到整个true 项目的入口函数,即main函数,位于cmd/getrue/main.go
内
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
main函数是getrue命令的入口函数,main.go文件中有一个**main() 和init()**函数,懂go语言的知道会先执行init()函数,init函数初始化配置一个解析命令的库。其中app.Action = getrue
则说明如果用户在没有输入其他的子命令的情况下会调用这个字段指向的函数app.Action = getrue,即main.go中的func getrue(ctx *cli.Context) 函数.然后执行main函数,调用 app.Run(os.Args) , os.Args 为系统参数(例: --datadir Test --nodiscover console)。我们来看看这个 app 到底是啥
func init() {
// 初始化CLI APP库 并且运行getrue
app.Action = getrue
app.HideVersion = true // we have a command to print the version
app.Copyright = "Copyright 2013-2018 The getrue Authors"
app.Commands = []cli.Command{
// See chaincmd.go:
initCommand,
importCommand,
exportCommand,
importPreimagesCommand,
exportPreimagesCommand,
...
根据main.go
的一段代码:var app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
,我们可以看到此app 是由 app = utils.newApp()
方法创建的,我们进入到 ‘cmd/utils’ 文件夹下面,可以看到newApp() 方法位于 flag.go
文件内
// NewApp creates an app with sane defaults.
func NewApp(gitCommit, usage string) *cli.App {
app := cli.NewApp() // 创建app
app.Name = filepath.Base(os.Args[0])
app.Author = ""
//app.Authors = nil
app.Email = ""
app.Version = params.Version
if len(gitCommit) >= 8 {
app.Version += "-" + gitCommit[:8]
}
app.Usage = usage
return app
}
我们追溯上面的cli.NewApp()
,会发现NewApp方法在是vender/gopkg.in/urfave/cli .v1/app.go
这个第三方的包内
// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
HelpName: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
Version: "0.0.0",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Writer: os.Stdout,
}
}
第三方包的大用法大致就是首先构造这个app对象,通过代码配置app对象的行为,提供一些回调函数。然后运行的时候直接在main函数里运行app.Run(os.Args)
账户地址生成
当我们使用personal.newAccount(password)
创建账户的时候如下:
上图我们可以看到 true-beta的地址,每个地址代表一个账户,每个true地址在true-beta网路中都是唯一存在的。 当我们输入新建账户的命令后,系统会给我们返回这个账户的地址,那true 是如何生成这串在全网唯一的字符串?那就得从入口文件cmd/getrue/main.go
分析
main.go的init()方法中有个命令初始化,accountCommand则是解析账户操作的命令,源码注释为账户命令详情在accountcmd.go文件内
func init() {
// Initialize the CLI app and start Getrue
app.Action = getrue
app.HideVersion = true // we have a command to print the version
app.Copyright = "Copyright 2013-2018 The getrue Authors"
app.Commands = []cli.Command{
// See chaincmd.go:
initCommand,
importCommand,
exportCommand,
importPreimagesCommand,
exportPreimagesCommand,
copydbCommand,
removedbCommand,
dumpCommand,
// See monitorcmd.go:
monitorCommand,
// See accountcmd.go:
// 账户命令详情在accountcmd.go 文件内
accountCommand,
新建账户的命令为new,会调用accountCreate 方法,源码位于cmd/getrue/accountcmd.go
文件内
{
Name: "new",
Usage: "Create a new account",
Action: utils.MigrateFlags(accountCreate),
Flags: []cli.Flag{
utils.DataDirFlag,
utils.KeyStoreDirFlag,
utils.PasswordFileFlag,
utils.LightKDFFlag,
},
Description: `
getrue account new
Creates a new account and prints the address.
The account is saved in encrypted format, you are prompted for a passphrase.
You must remember this passphrase to unlock your account in the future.
For non-interactive use the passphrase can be specified with the --password flag:
Note, this is meant to be used for testing only, it is a bad idea to save your
password to file or expose in any other way.
`,
},
......
// accountCreate creates a new account into the keystore defined by the CLI flags.
func accountCreate(ctx *cli.Context) error {
cfg := gethConfig{Node: defaultNodeConfig()}
// Load config file.
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
}
我们可以看到上面的代码中生成地址的代码是address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
,调用keystore.StoreKey()方法,传入4个参数,这4个参数的意思分别为:
- keydir: 生成的key 保存的位置
- password: 解锁账户的密码,是personal.newAccount(password)中的password.
- scryptN,scryptP :两个整型数scryptN,scryptP,这两个整型参数在keyStorePassphrase对象生命周期内部是固定不变的,只能在创建时赋值。
通过scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
这段代码可知 scryptN, scryptP, keydir 这3个值是通过账户配置文件配置而来,
我们再进入到keystore.StoreKey()方法中,源码位于accounts/keystore/keystore_passphrase.go
// StoreKey generates a key, encrypts with 'auth' and stores in the given directory
// 通过传入的变量,返回一个地址,和一个错误,若账户地址成功生成,那么 error = nil
func StoreKey(dir, auth string, scryptN, scryptP int) (common.Address, error) {
_, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP}, crand.Reader, auth)
return a.Address, err
}
StoreKey调用了一个storeNewKey()方法,storeNewKey方法中调用了newKey()方法。其源码位于accounts/keystore/key.go
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
}
.......
func newKey(rand io.Reader) (*Key, error) {
privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand)
if err != nil {
return nil, err
}
return newKeyFromECDSA(privateKeyECDSA), nil
}
......
func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
id := uuid.NewRandom()
key := &Key{
Id: id,
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
PrivateKey: privateKeyECDSA,
}
return key
}
在newKey方法中可以看到privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand) true采了椭圆曲线数字签名算法(ECDSA)生成私钥, 从newKeyFromECDSA方法中看出传入的私钥,通过crypto.PubkeyToAddress()生成地址,那么我们就要看看PubkeyToAddress()这个方法,此方法的源码位于crypto/crypto.go
func PubkeyToAddress(p ecdsa.PublicKey) common.Address {
pubBytes := FromECDSAPub(&p)
return common.BytesToAddress(Keccak256(pubBytes[1:])[12:])
}
......
// Keccak256 calculates and returns the Keccak256 hash of the input data.
func Keccak256(data ...[]byte) []byte {
d := sha3.NewKeccak256()
for _, b := range data {
d.Write(b)
}
return d.Sum(nil)
}
从上面的源码我们可以看出,通过传入的私钥,**pubBytes := FromECDSAPub(&p)**使用ECDSA算法推导出公钥,再将推导出的公钥经过Keccak-256单线散列函数推导出地址。最后一步步返回。
总结True-beta主网生成账户地址的过程:
- 通过椭圆曲线数字签名算法(ECDSA)创建随机私钥
- 从生成的随机私钥推导出公钥
- 从公钥推导出地址
有想了解非对称加密之椭圆曲线加密过程的可以自行百度。
当拿到了地址之后并且打印了地址。然后就是保存keystore 文件。通过解析启动节点时的–datadir 命令参数,即会配置config,将生成的keystore 保存至 --datadir 指定文件夹下的keystore 文件夹下。写入的源码位置为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
}
// 写入keystore 文件
return writeKeyFile(filename, keyjson)
}
...
// 这是将生成的账户地址信息保存为keystore.json
func writeKeyFile(file string, content []byte) error {
// Create the keystore directory with appropriate permissions
// in case it is not present yet.
// 定义生成的文件是否可读可写,可执行。0700代表当前用户可读可写可执行
const dirPerm = 0700
if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil {
return err
}
// Atomic write: create a temporary hidden file first
// then move it into place. TempFile assigns mode 0600.
f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp")
if err != nil {
return err
}
if _, err := f.Write(content); err != nil {
f.Close()
os.Remove(f.Name())
return err
}
f.Close()
return os.Rename(f.Name(), file)
}
这就是我主要解析的true-beta主网的生成账户地址并且保存生成的账户地址的keystore.json文件的源码。
true 的主要创新点在于 TrueChain采用PBFT+PoW混合共识,形成双链结构,将PBFT作为快链,而将PoW作为慢链,快链用于存储账本和达成交易,慢链用于挖矿和委员会选举,从而实现了公平和效率的有效融合,并使得数据结构变得更加清晰,使两种共识之间的通信变得更加容易。
其次原创性的推出TrueHash,实现不依赖手动调整,从而根本上抗ASIC的挖矿算法
再是 初链“交易处理担当”主要为PBFT委员会,为保证公平和安全性,初链PBFT委员会将定期换届trueChain实现了随机算法在换届时公平地选举PBFT委员,基于VRF(verify random function,可验证随机函数)可以实现PBFT节点等概率的公平选举。而这里面也会有一些过滤,过滤掉一些挖水果特别少的矿工,因为他们挖的特别少,可能他们的硬件设备或者网络情况不太好。从剩下满足处理交易条件的节点中,随机的公平的选举出来PBFT委员会成员。