以太坊源碼分析-轉賬流程分析


以太坊源碼分析-以太坊啟動
前面我們分析以太坊的啟動過程,在過程中已經創建了交易池(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(chainId*2)求和,其結果為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() > 32
1024 {
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狀態,等待處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM