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