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