以太坊源码分析-转账流程分析

时间:2022-01-09 22:20:48

以太坊源码分析-以太坊启动
前面我们分析以太坊的启动过程,在过程中已经创建了交易池(tx_pool),现在我们猜测一下转账的大概步骤:

创建一笔交易,并发送
接收到交易信息,然后做一些验证
验证合法,将该交易放入交易池,等待打包到Block中
首先,我们从命令行行模拟一个交易,账户A向账户B转账3ether,在转账前,我们需要先对账户A解锁授权,解锁命令如下:

personal.unlockAccount(eth.accounts[0])
输入密码即可解锁该账户。接下来,我们从A账户像B账户转账3以太币,转账命令如下:

eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:web3.toWei(3,'ether')})
sendTransaction接受一个json参数,其key分别对应的含义如下:

from:转出账户
to:转入账户
value:交易金额。以太坊的基本单位是维,1eth = pow(10,18)
sendTransaction经过RPC方式调用后,最终调用ethapi/api.go中的SendTransaction方法,该方法的实现逻辑如下:

func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
// Look up the wallet containing the requested signer
account := accounts.Account{Address: args.From}

wallet, err := s.am.Find(account)
if err != nil {
    return common.Hash{}, err
}

if args.Nonce == nil {
    // Hold the addresse's mutex around signing to prevent concurrent assignment of
    // the same nonce to multiple accounts.
    s.nonceLock.LockAddr(args.From)
    defer s.nonceLock.UnlockAddr(args.From)
}

// Set some sanity defaults and terminate on failure
if err := args.setDefaults(ctx, s.b); err != nil {
    return common.Hash{}, err
}
// Assemble the transaction and sign with the wallet
tx := args.toTransaction()

var chainID *big.Int
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
    chainID = config.ChainId
}
signed, err := wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
if err != nil {
    return common.Hash{}, err
}
return submitTransaction(ctx, s.b, signed)

}
首先,利用传入的参数from构造一个account变量,该变量代表转出方A,接着通过AccountManager获取该账户的wallet,wallet主要是对该交易进行签名,(关于AccountManager的创建,参考上一章以太坊源码分析-以太坊启动)
。接着调用setDefaults方法设置一些默认值,如果没有设置Gas,GasPrice,Nonce将会设置,这里提一下Nonce参数,该参数用户防双花攻击,对于每个账户,Nonce随着转账数的增加而增加。由于基本默认值都设置完成了,接下来就是利用这些值,创建一笔交易。生成一笔交易由toTransaction方法实现,该方法的实现如下:

func (args SendTxArgs) toTransaction() types.Transaction {
if args.To == nil {
return types.NewContractCreation(uint64(args.Nonce), (big.Int)(args.Value), (big.Int)(args.Gas), (big.Int)(args.GasPrice), args.Data)
}
return types.NewTransaction(uint64(args.Nonce), args.To, (big.Int)(args.Value), (big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
}
实现很简单,仅仅是判断是否To参数。对于合约而言,它是没有To值的;而对于我们发起的这笔转账,我们是一笔真实的从A用户向B用户转账,此时的To代表的就是账户B的地址。NewTransaction最终调用newTransaction创建一笔交易信息的,如下

func newTransaction(nonce uint64, to common.Address, amount, gasLimit, 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: new(big.Int),
Price: new(big.Int),
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
if amount != nil {
d.Amount.Set(amount)
}
if gasLimit != nil {
d.GasLimit.Set(gasLimit)
}
if gasPrice != nil {
d.Price.Set(gasPrice)
}

return &Transaction{data: d}

}
很简单,就是填充一些参数。现在交易变量已经创建好了,我们回到创建交易的变量的地方,接着分析。接着获取区块链的配置,检查是否是EIP155的区块号(关于以太坊第四次硬分叉修复重放攻击,参考EIP155).接着我们就对该笔交易签名来确保该笔交易的真实有效性。我们找到实现SignTx的keystore.go,实现签名的逻辑如下:

func (ks KeyStore) SignTx(a accounts.Account, tx types.Transaction, chainID big.Int) (types.Transaction, error) {
// Look up the key to sign with and abort if it cannot be found
ks.mu.RLock()
defer ks.mu.RUnlock()

unlockedKey, found := ks.unlocked[a.Address]
if !found {
    return nil, ErrLocked
}
// Depending on the presence of the chain ID, sign with EIP155 or homestead
if chainID != nil {
    return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
}
return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)

}
首先获取到所有已经解锁的账户,然后确认该当前账户是否解锁,如果没有解锁将异常退出。由于我们前面已经对A账户解锁,此时将能够在已解锁的账户中找到。接下来检查chainID,如果当前链的区块号在EIP155之前,由于我这里在初始化创世块时指定了chainID,因此此时将使用EIP155Signer签名。签名的代码如下:

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 s.WithSignature(tx, sig)
}
首先获取该交易的RLP编码哈希值,然后使用私钥对该值进行ECDSA签名处理。接着调用WithSignature来对交易的R、S、V初始化。EIP155Signer和HomesteadSigner如下:

EIP155Signer如下

func (s EIP155Signer) WithSignature(tx Transaction, sig []byte) (Transaction, error) {
if len(sig) != 65 {
panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
}

cpy := &Transaction{data: tx.data}
cpy.data.R = new(big.Int).SetBytes(sig[:32])
cpy.data.S = new(big.Int).SetBytes(sig[32:64])
cpy.data.V = new(big.Int).SetBytes([]byte{sig[64]})
if s.chainId.Sign() != 0 {
    cpy.data.V = big.NewInt(int64(sig[64] + 35))
    cpy.data.V.Add(cpy.data.V, s.chainIdMul)
}
return cpy, nil

}
HomesteadSigner如下

func (hs HomesteadSigner) WithSignature(tx Transaction, sig []byte) (Transaction, error) {
if len(sig) != 65 {
panic(fmt.Sprintf("wrong size for snature: got %d, want 65", len(sig)))
}
cpy := &Transaction{data: tx.data}
cpy.data.R = new(big.Int).SetBytes(sig[:32])
cpy.data.S = new(big.Int).SetBytes(sig[32:64])
cpy.data.V = new(big.Int).SetBytes([]byte{sig[64] + 27})
return cpy, nil
}
他们唯一的差别就是在V的处理上,对于EIP155Singer将签名的第64位转换成int然后加上35,在跟chainIdMul(chainId2)求和,其结果为V = int64(sig[64]) + 35 + chainId 2,对于我这里在初始化创世块是指定chainId=10,此时相当于V=int64(sig[64]) + 55.而对于HomesteadSigner的WithSignature计算很简单,仅仅是sig[64]+27。该值主要是预防重放攻击。整个签名就完成了,并重新包装生成一个带签名的交易变量。我们回到调用签名的地方,此时将签名后的交易提交出去,下面我们来看看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, _ := types.Sender(signer, tx)
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
}
该方法首先将该交易发送给backend处理,返回经过签名后交易的hash值。我们来看看发送给backend是如何处理该比交易的,该方法在api_backend.go中实现,该方法仅仅是转给tx_pool的AddLocal处理,在转给pool.addTx将该比交易放入到交易池等待处理,我们来看看其实现逻辑:

func (pool TxPool) addTx(tx types.Transaction, local bool) error {
pool.mu.Lock()
defer pool.mu.Unlock()

// Try to inject the transaction and update any state
replace, err := pool.add(tx, local)
if err != nil {
    return err
}
// If we added a new transaction, run promotion checks and return
if !replace {
    state, err := pool.currentState()
    if err != nil {
        return err
    }
    from, _ := types.Sender(pool.signer, tx) // already validated
    pool.promoteExecutables(state, []common.Address{from})
}
return nil

}
这里我们分两步来解释。第一步主要是调用add方法,将该交易放入交易池,add的实现如下:

func (pool TxPool) add(tx types.Transaction, local bool) (bool, error) {
// If the transaction is already known, discard it
hash := tx.Hash()
if pool.all[hash] != nil {
log.Trace("Discarding already known transaction", "hash", hash)
return false, fmt.Errorf("known transaction: %x", hash)
}
// If the transaction fails basic validation, discard it
if err := pool.validateTx(tx, local); err != nil {
log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
invalidTxCounter.Inc(1)
return false, err
}
// If the transaction pool is full, discard underpriced transactions
if uint64(len(pool.all)) >= pool.config.GlobalSlots+pool.config.GlobalQueue {
// If the new transaction is underpriced, don't accept it
if pool.priced.Underpriced(tx, pool.locals) {
log.Trace("Discarding underpriced transaction", "hash", hash, "price", tx.GasPrice())
underpricedTxCounter.Inc(1)
return false, ErrUnderpriced
}
// New transaction is better than our worse ones, make room for it
drop := pool.priced.Discard(len(pool.all)-int(pool.config.GlobalSlots+pool.config.GlobalQueue-1), pool.locals)
for , tx := range drop {
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "price", tx.GasPrice())
underpricedTxCounter.Inc(1)
pool.removeTx(tx.Hash())
}
}
// If the transaction is replacing an already pending one, do directly
from,
:= types.Sender(pool.signer, tx) // already validated
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
// Nonce already pending, check if required price bump is met
inserted, old := list.Add(tx, pool.config.PriceBump)
if !inserted {
pendingDiscardCounter.Inc(1)
return false, ErrReplaceUnderpriced
}
// New transaction is better, replace old one
if old != nil {
delete(pool.all, old.Hash())
pool.priced.Removed()
pendingReplaceCounter.Inc(1)
}
pool.all[tx.Hash()] = tx
pool.priced.Put(tx)

    log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
    return old != nil, nil
}
// New transaction isn't replacing a pending one, push into queue and potentially mark local
replace, err := pool.enqueueTx(hash, tx)
if err != nil {
    return false, err
}
if local {
    pool.locals.add(from)
}
log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
return replace, nil

}
该方法首先检查交易池是否已经存在该笔交易了,接下来调用validateTx对交易的合法性进行验证。接下来交易池是否超过容量。如果超过容量,首先检查该交易的交易费用是否低于当前交易列表的最小值,如果低于则拒绝该比交易;如果比其它交易高,则从已有的交易中移除一笔交易费用最低的交易,为当前这笔交易留出空间。接着继续检查该比交易的Nonce值,确认该用户下的交易是否存在该比交易,如果已经存在该比交易,则删除之前的交易,并将该比交易放入交易池中,然后返回。如果该用户下的交易列表中不含有该比交易,则调用enqueueTx将该比交易放入交易池中。如果该比交易是本地发出,需要将发送者(转出方)保存在交易池的locals中。接下来我们来看看validateTx对该比交易做了哪些验证:

func (pool TxPool) validateTx(tx types.Transaction, local bool) error {
// Heuristic limit, reject transactions over 32KB to prevent DOS attacks
if tx.Size() > 321024 {
return ErrOversizedData
}
// Transactions can't be negative. This may never happen using RLP decoded
// transactions but may occur if you create a transaction using the RPC.
if tx.Value().Sign() < 0 {
return ErrNegativeValue
}
// Ensure the transaction doesn't exceed the current block limit gas.
if pool.gasLimit().Cmp(tx.Gas()) < 0 {
return ErrGasLimit
}
// Make sure the transaction is signed properly
from, err := types.Sender(pool.signer, tx)
if err != nil {
return ErrInvalidSender
}
// Drop non-local transactions under our own minimal accepted gas price
local = local || pool.locals.contains(from) // account may be local even if the transaction arrived from the network
if !local && pool.gasPrice.Cmp(tx.GasPrice()) > 0 {
return ErrUnderpriced
}
// Ensure the transaction adheres to nonce ordering
currentState, err := pool.currentState()
if err != nil {
return err
}
if currentState.GetNonce(from) > tx.Nonce() {
return ErrNonceTooLow
}
// Transactor should have enough funds to cover the costs
// cost == V + GP
GL
if currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
return ErrInsufficientFunds
}
intrGas := IntrinsicGas(tx.Data(), tx.To() == nil, pool.homestead)
if tx.Gas().Cmp(intrGas) < 0 {
return ErrIntrinsicGas
}
return nil
}
主要是对一下几点进行验证:

验证该比交易的大小,如果大小大于32KB则拒绝该笔交易,这样做主要是防止DDOS攻击
接着验证转账金额,如果金额小于0则拒绝该笔无效交易
该笔交易的gas不能大于消息池gas的限制
该笔交易已经进行了正确的签名
如果该笔交易不是来自本地(来自其它节点)并且该交易的GasPrice小于当前交易池的GasPrice,则拒绝该笔交易。可见交易池是可以拒绝低GasPrice交易的
当前用户的nonce如果大于该笔交易的nonce,则拒绝
验证当前转出用户A的余额是否充足,如果不足拒绝。cost == V + GP * GL
验证该笔交易的固有花费,如果小于交易池的Gas,则拒绝该笔交易。相关的计算参考state_transaction.IntrinsicGas函数
以上就是对该交易的合法性的完整验证。接着我们回到第二步,在上面经过见证后,如果合法则将该笔交易添加到交易池,如果该笔交易原来不存在,则replace=false,此时执行promoteExecutables方法,该方法主要是将可处理的交易待处理(pending)列表,其实现如下:

func (pool TxPool) promoteExecutables(state state.StateDB, accounts []common.Address) {
gaslimit := pool.gasLimit()

// Gather all the accounts potentially needing updates
if accounts == nil {
    accounts = make([]common.Address, 0, len(pool.queue))
    for addr, _ := range pool.queue {
        accounts = append(accounts, addr)
    }
}
// Iterate over all accounts and promote any executable transactions
for _, addr := range accounts {
    list := pool.queue[addr]
    if list == nil {
        continue // Just in case someone calls with a non existing account
    }
    // Drop all transactions that are deemed too old (low nonce)
    for _, tx := range list.Forward(state.GetNonce(addr)) {
        hash := tx.Hash()
        log.Trace("Removed old queued transaction", "hash", hash)
        delete(pool.all, hash)
        pool.priced.Removed()
    }
    // Drop all transactions that are too costly (low balance or out of gas)
    drops, _ := list.Filter(state.GetBalance(addr), gaslimit)
    for _, tx := range drops {
        hash := tx.Hash()
        log.Trace("Removed unpayable queued transaction", "hash", hash)
        delete(pool.all, hash)
        pool.priced.Removed()
        queuedNofundsCounter.Inc(1)
    }
    // Gather all executable transactions and promote them
    for _, tx := range list.Ready(pool.pendingState.GetNonce(addr)) {
        hash := tx.Hash()
        log.Trace("Promoting queued transaction", "hash", hash)
        pool.promoteTx(addr, hash, tx)
    }
    // Drop all transactions over the allowed limit
    if !pool.locals.contains(addr) {
        for _, tx := range list.Cap(int(pool.config.AccountQueue)) {
            hash := tx.Hash()
            delete(pool.all, hash)
            pool.priced.Removed()
            queuedRateLimitCounter.Inc(1)
            log.Trace("Removed cap-exceeding queued transaction", "hash", hash)
        }
    }
    // Delete the entire queue entry if it became empty.
    if list.Empty() {
        delete(pool.queue, addr)
    }
}
// If the pending limit is overflown, start equalizing allowances
pending := uint64(0)
for _, list := range pool.pending {
    pending += uint64(list.Len())
}
if pending > pool.config.GlobalSlots {
    pendingBeforeCap := pending
    // Assemble a spam order to penalize large transactors first
    spammers := prque.New()
    for addr, list := range pool.pending {
        // Only evict transactions from high rollers
        if !pool.locals.contains(addr) && uint64(list.Len()) > pool.config.AccountSlots {
            spammers.Push(addr, float32(list.Len()))
        }
    }
    // Gradually drop transactions from offenders
    offenders := []common.Address{}
    for pending > pool.config.GlobalSlots && !spammers.Empty() {
        // Retrieve the next offender if not local address
        offender, _ := spammers.Pop()
        offenders = append(offenders, offender.(common.Address))

        // Equalize balances until all the same or below threshold
        if len(offenders) > 1 {
            // Calculate the equalization threshold for all current offenders
            threshold := pool.pending[offender.(common.Address)].Len()

            // Iteratively reduce all offenders until below limit or threshold reached
            for pending > pool.config.GlobalSlots && pool.pending[offenders[len(offenders)-2]].Len() > threshold {
                for i := 0; i < len(offenders)-1; i++ {
                    list := pool.pending[offenders[i]]
                    for _, tx := range list.Cap(list.Len() - 1) {
                        // Drop the transaction from the global pools too
                        hash := tx.Hash()
                        delete(pool.all, hash)
                        pool.priced.Removed()

                        // Update the account nonce to the dropped transaction
                        if nonce := tx.Nonce(); pool.pendingState.GetNonce(offenders[i]) > nonce {
                            pool.pendingState.SetNonce(offenders[i], nonce)
                        }
                        log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
                    }
                    pending--
                }
            }
        }
    }
    // If still above threshold, reduce to limit or min allowance
    if pending > pool.config.GlobalSlots && len(offenders) > 0 {
        for pending > pool.config.GlobalSlots && uint64(pool.pending[offenders[len(offenders)-1]].Len()) > pool.config.AccountSlots {
            for _, addr := range offenders {
                list := pool.pending[addr]
                for _, tx := range list.Cap(list.Len() - 1) {
                    // Drop the transaction from the global pools too
                    hash := tx.Hash()
                    delete(pool.all, hash)
                    pool.priced.Removed()

                    // Update the account nonce to the dropped transaction
                    if nonce := tx.Nonce(); pool.pendingState.GetNonce(addr) > nonce {
                        pool.pendingState.SetNonce(addr, nonce)
                    }
                    log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
                }
                pending--
            }
        }
    }
    pendingRateLimitCounter.Inc(int64(pendingBeforeCap - pending))
}
// If we've queued more transactions than the hard limit, drop oldest ones
queued := uint64(0)
for _, list := range pool.queue {
    queued += uint64(list.Len())
}
if queued > pool.config.GlobalQueue {
    // Sort all accounts with queued transactions by heartbeat
    addresses := make(addresssByHeartbeat, 0, len(pool.queue))
    for addr := range pool.queue {
        if !pool.locals.contains(addr) { // don't drop locals
            addresses = append(addresses, addressByHeartbeat{addr, pool.beats[addr]})
        }
    }
    sort.Sort(addresses)

    // Drop transactions until the total is below the limit or only locals remain
    for drop := queued - pool.config.GlobalQueue; drop > 0 && len(addresses) > 0; {
        addr := addresses[len(addresses)-1]
        list := pool.queue[addr.address]

        addresses = addresses[:len(addresses)-1]

        // Drop all transactions if they are less than the overflow
        if size := uint64(list.Len()); size <= drop {
            for _, tx := range list.Flatten() {
                pool.removeTx(tx.Hash())
            }
            drop -= size
            queuedRateLimitCounter.Inc(int64(size))
            continue
        }
        // Otherwise drop only last few transactions
        txs := list.Flatten()
        for i := len(txs) - 1; i >= 0 && drop > 0; i-- {
            pool.removeTx(txs[i].Hash())
            drop--
            queuedRateLimitCounter.Inc(1)
        }
    }
}

}
首先迭代所有当前账户的交易,检查当前交易的nonce是否太低(说明该笔交易不合法),如果太低则删除,接着检查余额不足或者gas不足的交易并删除,接着调用promoteTx方法,将该比交易的状态更新为penging并且放在penging集合中,然后将当前消息池该用户的nonce值+1,接着广播TxPreEvent事件,告诉他们本地有一笔新的合法交易等待处理。最终将通过handler.txBroadcastLoop 广播给其它节点,然后在整个以太坊网络上传播并被其它节点接收,等待验证。
接着检查消息池的pending列表是否超过容量,如果超过将进行扩容操作。如果一个账户进行的状态超过限制,从交易池中删除最先添加的交易。到此,发送一笔交易就分析完了,此时交易池中的交易等待挖矿打包处理,后面我们将分析挖矿打包处理,并执行状态转换函数(执行转账)的逻辑。下面我们在命令行看看刚才这笔交易的状态:

txpool.status
{
pending: 1,
queued: 0
}
可以看到有1笔交易处于penging状态,等待处理。