1. 程式人生 > >以太坊交易原始碼分析

以太坊交易原始碼分析

這篇開始分析以太坊交易相關程式碼。基本流程參見下圖:

可以看到,完整流程分為以下幾個步驟:

  • 發起交易:指定目標地址和交易金額,以及需要的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向所有沒有該交易的結點發送該交易。

或關注飛久微信公眾號: