go-ethereum交易发送流程源码分析

简介

如果只是为了测试,可以启动以太坊的rpc测试工具:ganache-cli。然后使用attach命令去连接测试节点geth attach http://127.0.0.1:8545。当然,你也可以利用geth搭建一个私链去测试,这里不再多做介绍,ganache-cli安装如下:

npm install -g ganache-cli

先介绍几个命令:

查看所有账户:eth.accounts
查看某个账户的余额:eth.getBalance(eth.accounts[0])
解锁某个账户:personal.unlockAccount(eth.accounts[0],"密码")
交易:eth.sendTransaction({from:**,to:**,value:**}) 

账户解锁

交易前必须解锁账户,所以我们先从这里开始分析。解锁账户的命令时:personal.unlockAccount。回顾之前geth启动流程,在启动服务时会启动rpc服务,此时会收集所有api

func (n *Node) startRPC(services map[reflect.Type]Service) error {
	apis := n.apis()
	for _, service := range services {
		apis = append(apis, service.APIs()...)
	}

	if err := n.startInProc(apis); err != nil {
		return err
	}
	if err := n.startIPC(apis); err != nil {
		n.stopInProc()
		return err
	}
	if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts, n.config.HTTPTimeouts); err != nil {
		n.stopIPC()
		n.stopInProc()
		return err
	}
	if err := n.startWS(n.wsEndpoint, apis, n.config.WSModules, n.config.WSOrigins, n.config.WSExposeAll); err != nil {
		n.stopHTTP()
		n.stopIPC()
		n.stopInProc()
		return err
	}

	n.rpcAPIs = apis
	return nil
}

第一步先收集node提供的api,之后再收集所有服务提供的api,由于默认只有ethereum服务,所有我们能用的api都来自这两个地方。API实际上是一个结构体

type API struct {
	Namespace string     
	Version   string      
	Service   interface{} 
	Public    bool        
}

他有四个成员,Namespace是命名空间,我们使用的personal.unlockAccount命令中personal就是一个命名空间,一个命名空间下有若干个实际的命令。Version就是版本号,Service就是该API具体逻辑的结构体。Public指该API是否对外公开。

回顾rpc源码,在启动rpc服务时会传入所有要提供服务的API,之后对每个api使用RegisterName进行注册,将API指定的结构体中可导出的每个方法包装成一个个callback,每个callback对应的名字就是方法名,只不过第一个字母小写。同一个结构体的所有可调用的方法包装成一个service,service的name字段就是API的Namespace。收到请求时就是根据命名空间及对应的方法名取查找对应callback来执行逻辑。

这里我们同过查找发现personal.unlockAccount的这个指令对应的PrivateAccountAPI结构体中的UnlockAccount方法

// go-ethereum\internal\ethapi\api.go
func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {
	const max = uint64(time.Duration(math.MaxInt64) / time.Second)
	var d time.Duration
	if duration == nil {
		d = 300 * time.Second
	} else if *duration > max {
		return false, errors.New("unlock duration too large")
	} else {
		d = time.Duration(*duration) * time.Second
	}
	err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)
	if err != nil {
		log.Warn("Failed account unlock attempt", "address", addr, "err", err)
	}
	return err == nil, err
}

这里我们发现实际上这个方法接收三个参数,但第三个参数可以为空。这三个参数分别是要解锁的账户地址,密码以及解锁状态持续时间。默认保持解锁300秒,可以自定义,但不能过大。接着主要逻辑开始,首先调用fetchKeystore方法去从accountmanager中获取一个keystore对象,之后调用其TimedUnlock方法。这个方法之前在分析geth的account子命令时分析过,解锁实际上就是用提供的密码解密账户对应的keystore文件,获得账户对应的私钥。并持续一定时间的解锁状态,所谓解锁状态是指地址对应的unlocked可以找得到,到期后将对应unlocked对象中的私钥清零,并将其移除unlocked集合。

发送交易

发送交易的实际代码如下

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()

	signed, err := wallet.SignTx(account, tx, s.b.ChainConfig().ChainID)
	if err != nil {
		return common.Hash{}, err
	}
	return SubmitTransaction(ctx, s.b, signed)
}

注意这个方法有两个参数,但是调用时我们没有指定第一个参数,由于不是基本类型,所以我们也无法指定第一个参数。回顾rpc源码。在注册服务时,如果方法的第一个参数是contextType类型,则跳过第一个参数并标记该callback的hasCtx为true。

SendTransaction方法的第二个参数也不是基本参数,但是我们查看SendTxArgs的定义

type SendTxArgs struct {
	From     common.Address  `json:"from"`
	To       *common.Address `json:"to"`
	Gas      *hexutil.Uint64 `json:"gas"`
	GasPrice *hexutil.Big    `json:"gasPrice"`
	Value    *hexutil.Big    `json:"value"`
	Nonce    *hexutil.Uint64 `json:"nonce"`

	Data  *hexutil.Bytes `json:"data"`
	Input *hexutil.Bytes `json:"input"`
}

SendTxArgs实际上是一个json对象,所以我们只要输入json格式的字符串即可,在处理请求时会自动序列化为json对象。

方法的第一步是先构造一个account对象,先只赋值地址参数。之后去寻找这个账户对于的钱包对象。找到的话,如果参数中的nonce字段为空,则调用nonceLock.LockAddr方法

func (l *AddrLocker) LockAddr(address common.Address) {
	l.lock(address).Lock()
}

func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.locks == nil {
		l.locks = make(map[common.Address]*sync.Mutex)
	}
	if _, ok := l.locks[address]; !ok {
		l.locks[address] = new(sync.Mutex)
	}
	return l.locks[address]
}

实际上就是给每个地址创建一个互斥锁并进行锁定。回到SendTransaction中,有调用了setDefaults进行参数补全

func (args *SendTxArgs) setDefaults(ctx context.Context, b Backend) error {
	if args.Gas == nil {
		args.Gas = new(hexutil.Uint64)
		*(*uint64)(args.Gas) = 90000
	}
	if args.GasPrice == nil {
		price, err := b.SuggestPrice(ctx)
		if err != nil {
			return err
		}
		args.GasPrice = (*hexutil.Big)(price)
	}
	if args.Value == nil {
		args.Value = new(hexutil.Big)
	}
	if args.Nonce == nil {
		nonce, err := b.GetPoolNonce(ctx, args.From)
		if err != nil {
			return err
		}
		args.Nonce = (*hexutil.Uint64)(&nonce)
	}
	if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
		return errors.New(`Both "data" and "input" are set and not equal. Please use "input" to pass transaction call data.`)
	}
	if args.To == nil {
		// Contract creation
		var input []byte
		if args.Data != nil {
			input = *args.Data
		} else if args.Input != nil {
			input = *args.Input
		}
		if len(input) == 0 {
			return errors.New(`contract creation without any data provided`)
		}
	}
	return nil
}

这个主要是补全SendTxArgs的一些空成员变量,也就是我们为提供的值,一般发生一个普通交易我们只要提供收发双方地址和交易金额即可。接着调用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)
}

首先Data与Input都表示交易携带的信息,Input是一个新名字而已。接着根据接收人地址是否为空构造不同的交易对象

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)
}

二者调用的实际上是一个方法,区别就是接受者的地址是否为空

func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction {
	if len(data) > 0 {
		data = common.CopyBytes(data)
	}
	d := txdata{
		AccountNonce: nonce,
		Recipient:    to,
		Payload:      data,
		Amount:       new(big.Int),
		GasLimit:     gasLimit,
		Price:        new(big.Int),
		V:            new(big.Int),
		R:            new(big.Int),
		S:            new(big.Int),
	}
	if amount != nil {
		d.Amount.Set(amount)
	}
	if gasPrice != nil {
		d.Price.Set(gasPrice)
	}

	return &Transaction{data: d}
}

这里先构建了一个txdata对象,包含了交易的一些基本信息,然后构建了一个Transaction对象。回到SendTransaction,这里调用了SignTx对交易进行签名

func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
	if !w.Contains(account) {
		return nil, accounts.ErrUnknownAccount
	}
	return w.keystore.SignTx(account, tx, chainID)
}

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)
}

先查找钱包中是否包含该账户,有的话调用KeyStore的SignTx方法。首先检查账户是否已解锁,然后调用SignTx方法签名,但根据是否指定网络ID来决定使用什么singer

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)
}

首先对交易计算hash值,去然后用对hash值进行签名。我们以HomesteadSigner为Signer分析一下。首先hash方法如下

func (fs FrontierSigner) Hash(tx *Transaction) common.Hash {
	return rlpHash([]interface{}{
		tx.data.AccountNonce,
		tx.data.Price,
		tx.data.GasLimit,
		tx.data.Recipient,
		tx.data.Amount,
		tx.data.Payload,
	})
}

func rlpHash(x interface{}) (h common.Hash) {
	hw := sha3.NewLegacyKeccak256()
	rlp.Encode(hw, x)
	hw.Sum(h[:0])
	return h
}

hash方法是将交易的账户nonce、gasPrice、gaslimit、接受者地址、交易金额以及额外数据这些信息先进行rlp编码,然后使用keccak256算法进行hash计算,得到256位hash值。

Sign方法就是ECDSA数字签名算法,根据输入的私钥,计算出一个长度为65的字节数组。包含着ECDSA数字签名的结果R、S、V。注意原始的ECDSA签名算法只有R和S,这里又多出一个V,它是第65个字节,被称作“恢复ID”。关于ECDSA算法介绍见这里

WithSignature方法如下

func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, error) {
	r, s, v, err := signer.SignatureValues(tx, sig)
	if err != nil {
		return nil, err
	}
	cpy := &Transaction{data: tx.data}
	cpy.data.R, cpy.data.S, cpy.data.V = r, s, v
	return cpy, nil
}

func (fs FrontierSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
	}
	r = new(big.Int).SetBytes(sig[:32])
	s = new(big.Int).SetBytes(sig[32:64])
	v = new(big.Int).SetBytes([]byte{sig[64] + 27})
	return r, s, v, nil
}

先从刚才计算得到的签名值中解析出e、s、v的值。然后给transaction的data对于字段赋值。最后返回交易。

回到SendTransaction,获得已签名的交易后调用SubmitTransaction方法提交交易

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
}

第一步SendTx方法如下

func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
	return b.eth.txPool.AddLocal(signedTx)
}

实际调用的是txpool的方法,

func (pool *TxPool) AddLocal(tx *types.Transaction) error {
	return pool.addTx(tx, !pool.config.NoLocals)
}

func (pool *TxPool) addTxs(txs []*types.Transaction, local bool) []error {
	pool.mu.Lock()
	defer pool.mu.Unlock()

	return pool.addTxsLocked(txs, local)
}

这里最终以本地模式将交易添加到交易池中,若是一个全新的交易,则还要额外调用promoteExecutables将该地址的一些交易放到可执行交易的队列中。

回到SubmitTransaction中,放到交易池中后,如果接收人地址为空,则表示是一个合约创建交易,这时计算合约地址并打印log。最后方法返回交易的hash。所谓交易的hash就是对交易的rlp编码值计算hash值。

到此发送交易的源码已经分析的差不多了,但是源码还未到此结束,前面只是将交易添加到了交易池中,我们再来看一下后续的流程

事件触发及后续

在使用txpool的add方法时,方法结尾在一个单独的goroutine中发送了一个NewTxsEvent事件,那么是在哪里注册的呢。这还要回到ProtocolManager源码中,回顾之前ProtocolManager的源码分析,在启动ProtocolManager时注册了NewTxsEvent事件,然后启动了一个单独的goroutine执行txBroadcastLoop来处理这个事件,具体源码及分析见此文

最终是将交易发送给所有peer,每个节点收到交易时调用AddRemotes方法将其添加到交易池中,这个方法和AddLocal类似,只是不适用本地模式而已。

func (pool *TxPool) AddRemotes(txs []*types.Transaction) []error {
	return pool.addTxs(txs, false)
}

除了ProtocolManager注册了该事件,还有miner/worker也注册了,这个在miner源码中再分析。

题图来自unsplash:https://unsplash.com/photos/z2icBh4A9i0