以太坊交易原始碼分析
這篇開始分析以太坊交易相關程式碼。基本流程參見下圖:
可以看到,完整流程分為以下幾個步驟:
- 發起交易:指定目標地址和交易金額,以及需要的gas/gaslimit
- 交易簽名:使用賬戶私鑰對交易進行簽名
- 提交交易:把交易加入到交易緩衝池txpool中(會先對交易簽名進行驗證)
- 廣播交易:通知EVM執行,同時把交易資訊廣播給其他結點
下面依次分析這幾個部分的原始碼。
1. 發起交易
使用者通過JSON RPC發起eth_sendTransaction請求,最終會呼叫PublicTransactionPoolAPI
的實現,程式碼位於internal/ethapi/api.go:
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} wallet, err := s.b.AccountManager().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.SignTx(account, tx, chainID) if err != nil { return common.Hash{}, err } return submitTransaction(ctx, s.b, signed) }
首先根據from地址查詢到對應的wallet,檢查一下引數值,然後做了以下3件事:
- 通過SendTxArgs.toTransaction()建立交易
- 通過Wallet.SignTx()對交易進行簽名
- 通過submitTransaction()提交交易
這裡先分析建立交易部分。先看一下SendTxArgs型別的定義(internal/ethapi/api.go):
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"` // We accept "data" and "input" for backwards-compatibility reasons. "input" is the // newer name and should be preferred by clients. Data *hexutil.Bytes `json:"data"` Input *hexutil.Bytes `json:"input"` }
可以看到是和JSON欄位相應的,包括了地址、gas、金額這些交易資訊,nonce是一個隨賬戶交易次數自增的數字,一般會自動填充。交易還可以攜帶一些額外資料,存放在data或者input欄位中,推薦用input,data是為了向後相容。
接著看一下它的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)
}
可以看到,如果目標地址為空的話,表示這是一個建立智慧合約的交易,呼叫NewContractCreation()。否則說明這是一個普通交易,呼叫NewTransaction()。不管呼叫哪個,最終都會生成一個Transaction例項,我們看一下Transaction型別的定義,程式碼位於core/types/transaction.go:
type Transaction struct {
data txdata
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
主要就是包含了一個txdata型別的欄位,其他3個都是快取。看一下txdata型別的定義:
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
可以看到,除了剛剛那些引數值,還有3個簽名欄位和1個hash欄位。需要注意的是,from地址並不包含在該結構中。
2. 交易簽名
建立完Transaction例項以後,會呼叫Wallet.SignTx()進行簽名。具體流程參見下圖:
可以看到,是先通過Keccak-256演算法計算交易資料的hash值,然後結合賬戶的私鑰,通過ECDSA(Elliptic Curve Digital Signature Algorithm),也就是橢圓曲線數字簽名演算法生成簽名資料。
這裡有個疑問,為什麼txdata裡只有接收方的地址(Recipient),沒有傳送方的地址呢?那我們如何知道這筆交易的發起人時誰呢?實際上傳送方的地址是可以根據交易資料以及簽名推算出來的,參見下圖:
至於為什麼不把傳送方地址放到txdata中,是為了故意隱藏傳送方資訊,還是為了減小資料量,就不得而知了。
下面開始分析程式碼。上一篇文章分析過,Wallet是一個介面,具體實現在keyStoreWallet中,程式碼位於accounts/keystore/keystore_wallet.go中:
func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
// Make sure the requested account is contained within
if account.Address != w.account.Address {
return nil, accounts.ErrUnknownAccount
}
if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
return nil, accounts.ErrUnknownAccount
}
// Account seems valid, request the keystore to sign
return w.keystore.SignTx(account, tx, chainID)
}
繼續跟蹤KeyStore的SignTx()函式,程式碼位於accounts/keystore/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)
}
這裡會首先判斷賬戶是否已經解鎖,如果已經解鎖的話就可以獲取它的私鑰。
然後建立簽名器,如果要符合EIP155規範的話,需要把chainID傳進去,也就是我們的“--networkid”命令列引數。
最後呼叫一個全域性函式SignTx()完成簽名,程式碼位於core/types/transaction_signing.go:
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)
}
主要分為3個步驟:
- 生成交易的hash值
- 根據hash值和私鑰生成簽名
- 把簽名資料填充到Transaction例項中
2.1 生成交易的hash值
以EIP155Signer為例,程式碼如下:
func (s EIP155Signer) 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,
s.chainId, uint(0), uint(0),
})
}
func rlpHash(x interface{}) (h common.Hash) {
hw := sha3.NewKeccak256()
rlp.Encode(hw, x)
hw.Sum(h[:0])
return h
}
可以看到,先用SHA3-256生成hash值,然後再進行RLP編碼。RLP是一種資料序列化方法,後面有時間再寫文章分析。
2.2 根據hash值和私鑰生成簽名
crypto.Sign()函式程式碼位於crypto/signature_cgo.go:
// Sign calculates an ECDSA signature.
// The produced signature is in the [R || S || V] format where V is 0 or 1.
func Sign(hash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
if len(hash) != 32 {
return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
}
seckey := math.PaddedBigBytes(prv.D, prv.Params().BitSize/8)
defer zeroBytes(seckey)
return secp256k1.Sign(hash, seckey)
}
這裡是通過ECDSA演算法生成簽名資料,水平有限就不繼續分析了。最終會返回的簽名是一個位元組陣列,按R / S / V的順序排列。
2.3 填充簽名資料
最後一步就是把簽名資料的這3個值填充到Transaction結構中了,看一下WithSignature()函式,程式碼位於core/types/transaction.go:
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
}
生成的簽名資料是位元組陣列型別,需要通過signer.SignatureValues()函式轉換成3個big.Int型別的資料,然後填充到Transaction結構的R / S / V欄位上。可以瞄一眼這個轉換函式:
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
}
第0~31位元組是R,第32~63位元組是S,第64位加上27就可以得到V。
3. 提交交易
簽名完成以後,就需要呼叫submitTransaction()函式提交到交易緩衝池txpool中。
在分析程式碼之前,先看下TxPool中的幾個重要欄位:
pending map[common.Address]*txList // All currently processable transactions
queue map[common.Address]*txList // Queued but non-processable transactions
all map[common.Hash]*types.Transaction // All transactions to allow lookups
priced *txPricedList // All transactions sorted by price
pending欄位中包含了當前所有可被處理的交易列表,而queue欄位中包含了所有不可被處理、也就是新加入進來的交易。它們是按賬號地址來組織的,每個地址對應一個txList,具體內部結構參見下圖:
可以看到txList內部包含一個txSortedMap結構,實現按nonce排序,其內部維護了兩張表:
- 一張是包含了所有Transaction的map,key是Transaction的nonce值。之前提到過,這個nonce是隨著賬戶的交易次數自增的一個數字,所以越新的交易,nonce值越高。
- 還有一張表是一個數組,包含了所有nonce值,其內部是進行過堆排序的(小頂堆),nonce值按照從大到小排列。每次呼叫heap.Pop()時會取出最小的nonce值,也就是最老的交易。
all欄位中包含了所有的交易列表,以交易的hash作為key。
priced欄位則是把all中的交易列表按照gas price從大到小排列,如果gas price一樣,則按照交易的nonce值從小到大排列。最終的目標是每次取出gas price最大、nonce最小的交易。
我們提交交易的目標是:先把交易放入queue中記錄在案,然後再從queue中選一部分放入pending中進行處理。如果發現txpool滿了,則依據priced中的排序,剔除低油價的交易。
另外,如果是本地(local)提交的交易,預設情況下會盡可能地保證被放入txpool中,除非顯式關閉該配置。
接著我們看一下txpool的預設配置:
var DefaultTxPoolConfig = TxPoolConfig{
Journal: "transactions.rlp",
Rejournal: time.Hour,
PriceLimit: 1,
PriceBump: 10,
AccountSlots: 16,
GlobalSlots: 4096,
AccountQueue: 64,
GlobalQueue: 1024,
Lifetime: 3 * time.Hour,
}
- GlobalSlots:pending列表的最大長度,預設4096筆
- AccountSlots:pending中每個賬戶儲存的交易數的閾值,超過這個數量可能會被認為是垃圾交易或者是攻擊者,多餘交易可能被丟棄
- GlobalQueue:queue列表的最大長度,預設1024筆
- AccountQueue:queue中每個賬戶允許儲存的最大交易數,超過會被丟棄,預設64筆
- PriceLimit:允許進入txpool的最低gas price,預設1 Gwei
- PriceBump:如果出現兩個nonce相同的交易,gas price的差值超過該閾值則用新交易替換老交易
好,現在我們回到internal/ethapi/api.go,分析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
}
這裡有一個Backend引數,是在eth Service初始化時建立的,具體實現在EthApiBackend中,程式碼位於eth/api_backend.go。可以看到,這裡先呼叫了SendTx()函式提交交易,然後如果發現目標地址為空,表明這是一個建立智慧合約的交易,會建立合約地址。下面分別進行分析。
3.1 提交交易到txpool
func (b *EthApiBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
return b.eth.txPool.AddLocal(signedTx)
}
繼續跟蹤TxPool的AddLocal()函式:
func (pool *TxPool) AddLocal(tx *types.Transaction) error {
return pool.addTx(tx, !pool.config.NoLocals)
}
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 {
from, _ := types.Sender(pool.signer, tx) // already validated
pool.promoteExecutables([]common.Address{from})
}
return nil
}
這裡有兩個主要函式:add()和promoteExecuteables()。
add()會判斷是否應該把當前交易加入到queue列表中,promoteExecuteables()則會從queue中選取一些交易放入pending列表中等待執行。下面分別討論這兩個函式。
3.1.1 TxPool.add()
這個函式比較長,我們分成一段一段的來分析:
// 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)
}
這一段是先計算交易的hash值,然後判斷是不是已經在txpool 中,在的話就直接退出。
if err := pool.validateTx(tx, local); err != nil {
log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
invalidTxCounter.Inc(1)
return false, err
}
這一段是驗證交易的有效性,主要進行以下幾個方面的檢查:
- 資料量必須<32KB
- 交易金額必須非負(>=0)
- 交易的gas limit必須低於block的gas limit
- 簽名資料必須有效,能夠解析出傳送者地址
- 交易的gas price必須高於pool設定的最低gas price(除非是本地交易)
- 交易的nonce值必須高於當前鏈上該賬戶的nonce值(低於則說明這筆交易已經被打包過了)
- 當前賬戶餘額必須大於“交易金額 + gasprice * gaslimit”
- 交易的gas limit必須大於對應資料量所需的最低gas水平
// 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 !local && 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(), false)
}
}
這一段是在當前txpool已滿的情況下,剔除掉低油價的交易。還記得之前有個priced欄位儲存了按gas price以及nonce排序的交易列表嗎?這裡會先把當前交易的gas price和當前池中的最低價進行比較:
- 如果低於最低價,直接丟棄該交易返回
- 如果高於最低價,則從txpool中剔除一些低價的交易
// 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)
pool.journalTx(from, tx)
log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
// We've directly injected a replacement transaction, notify subsystems
go pool.txFeed.Send(TxPreEvent{tx})
return old != nil, nil
}
這一段是為了處理兩個交易nonce相同的問題。如果使用者發起了一筆交易,在還沒有被執行之前又用同樣的nonce發起了另一筆交易,則只會保留gas price高的那一筆。這個list.Overlaps()函式就是用來判斷pending列表中是否包含相同nonce的交易的。
// New transaction isn't replacing a pending one, push into queue
replace, err := pool.enqueueTx(hash, tx)
if err != nil {
return false, err
}
如果之前的那些檢查都沒有問題,就真正呼叫enqueueTx()函式把交易加入到queue列表中了。
// Mark local addresses and journal local transactions
if local {
pool.locals.add(from)
}
pool.journalTx(from, tx)
最後,如果發現這個賬戶是本地的,就把它加到一個白名單裡,預設會保證本地交易優先被加到txpool中。
至此,TxPool.add()函式就分析完了。
3.1.2 TxPool.promoteExecuteables()
這個函式比上面那個還長。。。主要目的是把交易從queue列表“提拔”到pending列表,程式碼邏輯比較清楚,具體可以參見下面這張圖:
根據不同的目的可以分為3塊,分別以粉色、紫色、綠色標識。
粉色部分主要是為了把queue中的交易“提拔”到pending中。當然在這之前需要先要進行一番檢查:
- 丟棄nonce < 賬戶當前nonce的交易,也就是已經被打包過的交易
- 丟棄轉賬金額 + gas消耗 > 賬戶餘額的交易,也就是會out-of-gas的交易
- 丟棄gas limit > block gas limit的交易,這部分交易可能會導致區塊生成失敗
紫色部分主要是為了清理pending列表,使其滿足GlobalSlots和AccountSlots的限制條件:
- 如果有些賬戶的交易數超過了AccountSlots,則先按交易數最少的賬戶進行均衡。舉例來說,如果有10個賬戶交易數超過了AccountSlots(預設16),其中交易數最少的賬戶包含20筆交易,那麼先把其他9個賬戶的交易數量削減到20。
- 如果經過上面的步驟,pending的長度還是超過了GlobalSlots,那就嚴格按照AccountSlots進行均衡,也就是把上面的10個賬戶的交易數進一步削減到16。
綠色部分主要是為了清理queue列表,使其滿足GlobalQueue和AccountQueue的限制條件:
- 如果每個賬戶的交易數超過了AccountQueue,丟棄多餘交易
- 如果queue的長度超過了GlobalQueue,則把賬戶按最後一次心跳時間排序,然後依次去除賬戶中的交易,直到滿足限制條件位置。
這裡提到一個最後一次心跳時間,其實就是賬戶最近一次交易的時間,用來作為賬戶活躍度的判斷
具體程式碼非常長,就不貼了,可以按照上面的圖自行對照。
3.2 建立智慧合約地址
再貼一下之前建立智慧合約地址的程式碼:
addr := crypto.CreateAddress(from, tx.Nonce())
引數是傳送方地址和交易的nonce值,然後呼叫CreateAddress()方法,程式碼位於crypto/crypto.go:
func CreateAddress(b common.Address, nonce uint64) common.Address {
data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
return common.BytesToAddress(Keccak256(data)[12:])
}
可以看到,就是先對剛剛兩個引數進行RLP編碼,然後計算hash值,取後20位作為合約地址。
至此,提交交易部分的程式碼就分析完了。
4. 廣播交易
交易提交到txpool中後,還需要廣播出去,一方面通知EVM執行該交易,另一方面要把交易資訊廣播給其他結點。具體呼叫在3.1.2節中提到的promoteTx()函式中,程式碼位於crypto/tx_pool.go:
func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.Transaction) {
……
// Set the potentially new pending nonce and notify any subsystems of the new tx
pool.beats[addr] = time.Now()
pool.pendingState.SetNonce(addr, tx.Nonce()+1)
go pool.txFeed.Send(TxPreEvent{tx})
}
可以看到,先更新了最後一次心跳時間,然後更新賬戶的nonce值,最後一行就是傳送一個TxPreEvent事件,外部可以通過SubscribeTxPreEvent()函式訂閱該事件:
func (pool *TxPool) SubscribeTxPreEvent(ch chan<- TxPreEvent) event.Subscription {
return pool.scope.Track(pool.txFeed.Subscribe(ch))
}
我們只要搜尋一下這個函式,就可以知道哪些元件訂閱了該事件了。
4.1 執行交易
第一個訂閱的地方位於miner/worker.go:
func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker {
……
// Subscribe TxPreEvent for tx pool
worker.txSub = eth.TxPool().SubscribeTxPreEvent(worker.txCh)
……
go worker.update()
……
}
開啟了一個goroutine來接收TxPreEvent,看一下update()函式:
func (self *worker) update() {
……
// Handle TxPreEvent
case ev := <-self.txCh:
// Apply transaction to the pending state if we're not mining
if atomic.LoadInt32(&self.mining) == 0 {
self.currentMu.Lock()
acc, _ := types.Sender(self.current.signer, ev.Tx)
txs := map[common.Address]types.Transactions{acc: {ev.Tx}}
txset := types.NewTransactionsByPriceAndNonce(self.current.signer, txs)
self.current.commitTransactions(self.mux, txset, self.chain, self.coinbase)
self.updateSnapshot()
self.currentMu.Unlock()
} else {
// If we're mining, but nothing is being processed, wake on new transactions
if self.config.Clique != nil && self.config.Clique.Period == 0 {
self.commitNewWork()
}
}
……
}
可以看到,如果結點不挖礦的話,這裡會立即呼叫commitTransactions()提交給EVM執行,獲得本地回執。
如果結點挖礦的話,miner會呼叫commitNewWork(),內部也會呼叫commitTransactions()執行交易。
4.2 廣播給其他結點
另一個訂閱的地方位於eth/handler.go:
func (pm *ProtocolManager) Start(maxPeers int) {
……
pm.txSub = pm.txpool.SubscribeTxPreEvent(pm.txCh)
go pm.txBroadcastLoop()
……
}
同樣也是啟動了一個goroutine來接收TxPreEvent事件,看一下txBroadcastLoop()函式:
func (pm *ProtocolManager) txBroadcastLoop() {
for {
select {
case event := <-pm.txCh:
pm.BroadcastTx(event.Tx.Hash(), event.Tx)
// Err() channel will be closed when unsubscribing.
case <-pm.txSub.Err():
return
}
}
}
繼續跟蹤BroadcastTx()函式:
func (pm *ProtocolManager) BroadcastTx(hash common.Hash, tx *types.Transaction) {
// Broadcast transaction to a batch of peers not knowing about it
peers := pm.peers.PeersWithoutTx(hash)
//FIXME include this again: peers = peers[:int(math.Sqrt(float64(len(peers))))]
for _, peer := range peers {
peer.SendTransactions(types.Transactions{tx})
}
log.Trace("Broadcast transaction", "hash", hash, "recipients", len(peers))
}
可以看到,這裡會通過P2P向所有沒有該交易的結點發送該交易。
或關注飛久微信公眾號: