以太坊源码阅读---一笔交易从生到死(一) txpool

   markdown太难写了记不住。跑到博客园来写写 今年还好 不是原地踏步的一年,最近在家办公工作转成半个区块链安全工程师(依旧不热爱这个行业,但是区块链技术还是挺有意思的)

   先小总结下

   1.学了solidity(入门容易 语法简单)

    2.简单学了学golang语法(写个小脚本啥的不难 鸭子类型看得有点绕)

    3.阅读、梳理了以太坊源码(30%进度吧  rlp解码没看 矿工看了一点  共识引擎没看  p2p简单看了下 生命周期 接口管理 evm没细看 tx基本看了下   差不多就这些? ) 很伟大的工程 坑也挺多的。

简单总结下一笔交易流程 a->b转账

在节点控制台的话直接调用eth.sendTransaction( 就好了 然而我们一般都用小狐狸  

 

小狐狸发的就是 "method":"eth_SendTransaction"  

以太坊的接口怎么找对应的方法呢 在注册的时候会把api注册进去  有两个地方会注册  一个是启动的时候node.New里面会调用node.rpcAPIs = append(node.rpcAPIs, node.apis()...)方法还有就是 eth.New(stack, cfg)的时候也会调用stack.RegisterAPIs(eth.APIs())  方法

转账接口就注册在这里面  以太坊根据method 以"_"切割前面是namespace  后面是对应的方法

复制代码
func (s *Ethereum) APIs() []rpc.API {
    apis := ethapi.GetAPIs(s.APIBackend) //  !!

    // Append any APIs exposed explicitly by the consensus engine
    apis = append(apis, s.engine.APIs(s.BlockChain())...)

    // Append all the local APIs and return
    return append(apis, []rpc.API{
        {
            Namespace: "eth",
            Service:   NewEthereumAPI(s),
        }, {
            Namespace: "miner",
            Service:   NewMinerAPI(s),
        }, {
            Namespace: "eth",
            Service:   downloader.NewDownloaderAPI(s.handler.downloader, s.eventMux),
        }, {
            Namespace: "admin",
            Service:   NewAdminAPI(s),
        }, {
            Namespace: "debug",
            Service:   NewDebugAPI(s),
        }, {
            Namespace: "net",
            Service:   s.netRPCService,
        },
    }...)
}
复制代码

转账还不在这 apis := ethapi.GetAPIs(s.APIBackend)在这里 

            Namespace: "eth", //  eth_xxxx
            Service:   NewTransactionAPI(apiBackend, nonceLock),

 

 NewTransactionAPI  返回的TransactionAPI  结构体

然后看下哪个方法绑定了TransactionAPI结构体并且方法名叫SendTransaction就好了

在internal/ethapi/api.go这里了

复制代码
func (s *TransactionAPI) SendTransaction(ctx context.Context, args TransactionArgs) (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()

    signed, err := wallet.SignTx(account, tx, s.b.ChainConfig().ChainID)
    if err != nil {
        return common.Hash{}, err
    }
    return SubmitTransaction(ctx, s.b, signed)
}
复制代码

代码先是本地找下参数中的from  然后如果没传nonce的话加个nonce锁这里说是防止同一个随机数分发 

接下来设置些默认属性 setDefaults 里面有些判断啥的比如to为空data也是空那么就报错(to为空就是创建合约 )

然后把参数设置到Transaction结构体里面 SubmitTransaction 看着是提交其实哪有那么容易

复制代码
func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
    // If the transaction fee cap is already specified, ensure the
    // fee of the given transaction is _reasonable_.
    if err := checkTxFee(tx.GasPrice(), tx.Gas(), b.RPCTxFeeCap()); err != nil {
        return common.Hash{}, err
    }
    if !b.UnprotectedAllowed() && !tx.Protected() {
        // Ensure only eip155 signed transactions are submitted if EIP155Required is set.
        return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC")
    }
    if err := b.SendTx(ctx, tx); err != nil { // EthAPIBackend   提交到   txpool
        return common.Hash{}, err
    }
    // Print a log with full tx details for manual investigations and interventions
    signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
    from, err := types.Sender(signer, tx)
    if err != nil {
        return common.Hash{}, err
    }

    if tx.To() == nil {
        addr := crypto.CreateAddress(from, tx.Nonce())
        log.Info("Submitted contract creation", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "contract", addr.Hex(), "value", tx.Value())
    } else {
        log.Info("Submitted transaction", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "recipient", tx.To(), "value", tx.Value())
    }
    return tx.Hash(), nil
}
复制代码

先校验手续费 然后是eip155 的校验 防止重放攻击

接下来就是b.SendTx 提交到pool池子里 SendTx有两种实现 一种是les轻节点一种是全节点  我们看全节点就好 一路跟

复制代码
func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error {
    // Filter out known ones without obtaining the pool lock or recovering signatures
    var (
        errs = make([]error, len(txs))
        news = make([]*types.Transaction, 0, len(txs))
    )
    for i, tx := range txs {
        // If the transaction is known, pre-set the error slot
        if pool.all.Get(tx.Hash()) != nil { //  限制下提交了在提交没用了
            errs[i] = ErrAlreadyKnown
            knownTxMeter.Mark(1)
            continue
        }
        // Exclude transactions with invalid signatures as soon as
        // possible and cache senders in transactions before
        // obtaining lock
        _, err := types.Sender(pool.signer, tx)
        if err != nil {
            errs[i] = ErrInvalidSender
            invalidTxMeter.Mark(1)
            continue
        }
        // Accumulate all unknown transactions for deeper processing
        news = append(news, tx)
    }
    if len(news) == 0 {
        return errs
    }

    // Process all the new transaction and merge any errors into the original slice
    pool.mu.Lock()
    newErrs, dirtyAddrs := pool.addTxsLocked(news, local)
    pool.mu.Unlock()

    var nilSlot = 0
    for _, err := range newErrs {
        for errs[nilSlot] != nil {
            nilSlot++
        }
        errs[nilSlot] = err
        nilSlot++
    }
    // Reorg the pool internals if needed and return
    done := pool.requestPromoteExecutables(dirtyAddrs)
    if sync {
        <-done
    }
    return errs
}
复制代码

先是for循环 校验重复提交、签名

接下来加锁 pool.addTxsLocked(news, local) 提交

 

复制代码
func (pool *TxPool) addTxsLocked(txs []*types.Transaction, local bool) ([]error, *accountSet) {
    dirty := newAccountSet(pool.signer)
    errs := make([]error, len(txs))
    for i, tx := range txs {
        replaced, err := pool.add(tx, local) // 提交
        errs[i] = err
        if err == nil && !replaced {
            dirty.addTx(tx)
        }
    }
    validTxMeter.Mark(int64(len(dirty.accounts)))
    return errs, dirty
}
复制代码

pool.add(tx, local) 提交

复制代码
func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err error) {
    // If the transaction is already known, discard it
    hash := tx.Hash() // 检查当前的交易是否已经知晓
    if pool.all.Get(hash) != nil {
        log.Trace("Discarding already known transaction", "hash", hash)
        knownTxMeter.Mark(1)
        return false, ErrAlreadyKnown
    }
    // Make the local flag. If it's from local source or it's from the network but
    // the sender is marked as local previously, treat it as the local transaction.
    isLocal := local || pool.locals.containsTx(tx)

    // If the transaction fails basic validation, discard it
    if err := pool.validateTx(tx, isLocal); err != nil { // 验证 重点
        log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
        invalidTxMeter.Mark(1)
        return false, err
    }
    // If the transaction pool is full, discard underpriced transactions   检查交易池 满了放弃低gas交易   没细看
    if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue {
        // If the new transaction is underpriced, don't accept it
        if !isLocal && pool.priced.Underpriced(tx) {
            log.Trace("Discarding underpriced transaction", "hash", hash, "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
            underpricedTxMeter.Mark(1)
            return false, ErrUnderpriced
        }
        // We're about to replace a transaction. The reorg does a more thorough
        // analysis of what to remove and how, but it runs async. We don't want to
        // do too many replacements between reorg-runs, so we cap the number of
        // replacements to 25% of the slots
        if pool.changesSinceReorg > int(pool.config.GlobalSlots/4) {
            throttleTxMeter.Mark(1)
            return false, ErrTxPoolOverflow
        }

        // New transaction is better than our worse ones, make room for it.
        // If it's a local transaction, forcibly discard all available transactions.
        // Otherwise if we can't make enough room for new one, abort the operation.
        drop, success := pool.priced.Discard(pool.all.Slots()-int(pool.config.GlobalSlots+pool.config.GlobalQueue)+numSlots(tx), isLocal)

        // Special case, we still can't make the room for the new remote one.
        if !isLocal && !success {
            log.Trace("Discarding overflown transaction", "hash", hash)
            overflowedTxMeter.Mark(1)
            return false, ErrTxPoolOverflow
        }
        // Bump the counter of rejections-since-reorg
        pool.changesSinceReorg += len(drop)
        // Kick out the underpriced remote transactions.
        for _, tx := range drop {
            log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
            underpricedTxMeter.Mark(1)
            pool.removeTx(tx.Hash(), false)
        }
    }
    // Try to replace an existing transaction in the pending pool
    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
        //         这里做  nonce 相同的 tx 替换  (替换指的是  替换 pending 中的tx)
        //         Nonce已经挂起,检查是否满足所需的价格暴涨
        //         PriceBump (默认为: 10)
        inserted, old := list.Add(tx, pool.config.PriceBump) // 提交
        if !inserted {
            pendingDiscardMeter.Mark(1)
            return false, ErrReplaceUnderpriced
        }
        // New transaction is better, replace old one
        if old != nil {
            pool.all.Remove(old.Hash())
            pool.priced.Removed(1)
            pendingReplaceMeter.Mark(1)
        }

        pool.all.Add(tx, isLocal)
        pool.priced.Put(tx, isLocal)
        // 将属于本地账户的tx写入磁盘
        pool.journalTx(from, tx)
        ///  新的重新排队?
        pool.queueTxEvent(tx)
        log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())

        // Successful promotion, bump the heartbeat
        pool.beats[from] = time.Now()
        return old != nil, nil
    }
    // New transaction isn't replacing a pending one, push into queue
    replaced, err = pool.enqueueTx(hash, tx, isLocal, true)
    if err != nil {
        return false, err
    }
    // Mark local addresses and journal local transactions
    if local && !pool.locals.contains(from) {
        log.Info("Setting new local account", "address", from)
        pool.locals.add(from)
        pool.priced.Removed(pool.all.RemoteToLocals(pool.locals)) // Migrate the remotes if it's marked as local first time.
    }
    if isLocal {
        localGauge.Inc(1)
    }
    pool.journalTx(from, tx)

    log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
    return replaced, nil
}
复制代码

提交代码巨特么长

先是校验hash  又来一遍

pool.validateTx 验证tx  这个是重点 等会再说

然后是交易池子满了的话放弃低gas交易

接下来如果nonce相同 且在pending池子里面 把gas高得替换了 这里替换的是pending的tx  有点绕其实这就是抢交易的问题  以太坊pool池子里面 有两种字典 一种是pending 一种是queue  pending顾名思义就是等待执行的tx 另一种queue就是在排队的tx  pending高于queue

 queueTxEvent 排队也就是把tx扔到queueTxEventCh通道里面

如果交易是新的没走上面流程那么久调用enqueuetx 把交易扔到queue池子里面  queue池子同样也可能存在同nonce的交易  如果有并且替换会返回一个bool  ture然后将这个tx扔到reqPromoteCh 来做交易升级处理 即queue->pending

scheduleReorgLoop 

复制代码
func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirtyAccounts *accountSet, events map[common.Address]*txSortedMap) {
    defer func(t0 time.Time) {
        reorgDurationTimer.Update(time.Since(t0))
    }(time.Now())
    defer close(done)

    var promoteAddrs []common.Address
    if dirtyAccounts != nil && reset == nil {
        // Only dirty accounts need to be promoted, unless we're resetting.
        // For resets, all addresses in the tx queue will be promoted and
        // the flatten operation can be avoided.
        promoteAddrs = dirtyAccounts.flatten()
    }
    pool.mu.Lock()
    if reset != nil {
        // Reset from the old head to the new, rescheduling any reorged transactions
        pool.reset(reset.oldHead, reset.newHead) //重置交易池子

        // Nonces were reset, discard any events that became stale
        for addr := range events {
            events[addr].Forward(pool.pendingNonces.get(addr))
            if events[addr].Len() == 0 {
                delete(events, addr)
            }
        }
        // Reset needs promote for all addresses
        promoteAddrs = make([]common.Address, 0, len(pool.queue))
        for addr := range pool.queue {
            promoteAddrs = append(promoteAddrs, addr)
        }
    }
    // Check for pending transactions for every account that sent new ones  //检查  交易升级
    promoted := pool.promoteExecutables(promoteAddrs)

    // If a new block appeared, validate the pool of pending transactions. This will
    // remove any transaction that has been included in the block or was invalidated
    // because of another transaction (e.g. higher gas price).
    if reset != nil {
        pool.demoteUnexecutables() //交易降级
        if reset.newHead != nil && pool.chainconfig.IsLondon(new(big.Int).Add(reset.newHead.Number, big.NewInt(1))) {
            pendingBaseFee := misc.CalcBaseFee(pool.chainconfig, reset.newHead)
            pool.priced.SetBaseFee(pendingBaseFee)
        }
        // Update all accounts to the latest known pending nonce
        nonces := make(map[common.Address]uint64, len(pool.pending))
        for addr, list := range pool.pending {
            highestPending := list.LastElement()
            nonces[addr] = highestPending.Nonce() + 1
        }
        pool.pendingNonces.setAll(nonces)
    }
    // Ensure pool.queue and pool.pending sizes stay within the configured limits.
    pool.truncatePending()
    pool.truncateQueue()

    dropBetweenReorgHistogram.Update(int64(pool.changesSinceReorg))
    pool.changesSinceReorg = 0 // Reset change counter
    pool.mu.Unlock()

    // Notify subsystems for newly added transactions
    for _, tx := range promoted {
        addr, _ := types.Sender(pool.signer, tx)
        if _, ok := events[addr]; !ok {
            events[addr] = newTxSortedMap()
        }
        events[addr].Put(tx)
    }
    if len(events) > 0 {
        var txs []*types.Transaction
        for _, set := range events {
            txs = append(txs, set.Flatten()...)
        }
        pool.txFeed.Send(NewTxsEvent{txs})
    }
}
复制代码

 

循环里会开个协程来执行runReorg操作  主要是交易升降级的处理  以及关键的txfeed订阅事件发送  升降级处理没啥好说的 就是pending->queue转换  最后的tx都会通过订阅机制发送出去

txFeed即为SubscribeNewTxsEvent的订阅   订阅SubscribeNewTxsEvent事件的一共有两个程序 一个是广播程序 另一个就是矿工

worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)

 矿工接下来就是执行evm操作了 这个篇幅也很长 下一篇在写

  

 

 
 
 
 
 
posted @   bfpiaoran  阅读(221)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示