这篇文章上次修改于 293 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

每个函数中只贴出了关键代码。

启动节点

启动 Arb node

func main() {
    os.Exit(mainImpl())
}

创建执行层

func mainImpl() int {
    execNode, err := gethexec.CreateExecutionNode(
        ctx,
        stack,
        chainDb,
        l2BlockChain,
        l1Client,
        func() *gethexec.Config { return &liveNodeConfig.Get().Execution },
    )
}

创建 sequencer

sequencer 拥有 PublishTransaction()

func CreateExecutionNode(
    ctx context.Context,
    stack *node.Node,
    chainDB ethdb.Database,
    l2BlockChain *core.BlockChain,
    l1client arbutil.L1Interface,
    configFetcher ConfigFetcher,
) (*ExecutionNode, error) {
    execEngine, err := NewExecutionEngine(l2BlockChain)

    if config.Sequencer.Enable {
        seqConfigFetcher := func() *SequencerConfig { return &configFetcher().Sequencer }
        sequencer, err = NewSequencer(execEngine, parentChainReader, seqConfigFetcher)
        if err != nil {
            return nil, err
        }
        txPublisher = sequencer
    }
    
    txPublisher = NewTxPreChecker(txPublisher, l2BlockChain, txprecheckConfigFetcher)
    arbInterface, err := NewArbInterface(execEngine, txPublisher)
    
    backend, filterSystem, err := arbitrum.NewBackend(stack, &config.RPC, chainDB, arbInterface, filterConfig)
}

交易处理过程

提交交易

// Transaction pool API
func (a *APIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
   return a.b.EnqueueL2Message(ctx, signedTx, nil)
}

func (b *Backend) EnqueueL2Message(ctx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error {
   return b.arb.PublishTransaction(ctx, tx, options)
}

交易进入 sequencer 队列

func (s *Sequencer) PublishTransaction(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error {
    sequencerBacklogGauge.Inc(1)
    defer sequencerBacklogGauge.Dec(1)
    ...
    queueItem := txQueueItem{
        tx,
        options,
        resultChan,
        false,
        queueCtx,
        time.Now(),
    }
    select {
    case s.txQueue <- queueItem:
}

取出交易

func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) {
    var queueItems []txQueueItem
    var totalBatchSize int
    ...
    case queueItem = <-s.txQueue:
    ...
    queueItems = s.precheckNonces(queueItems)
    txes := make([]*types.Transaction, len(queueItems))

    block, err := s.execEngine.SequenceTransactions(header, txes, hooks)
}

func (s *ExecutionEngine) SequenceTransactions(header *arbostypes.L1IncomingMessageHeader, txes types.Transactions, hooks *arbos.SequencingHooks) (*types.Block, error) {
    return s.sequencerWrapper(func() (*types.Block, error) {
        hooks.TxErrors = nil
        return s.sequenceTransactionsWithBlockMutex(header, txes, hooks)
    })
}

处理交易

func (s *ExecutionEngine) sequenceTransactionsWithBlockMutex(header *arbostypes.L1IncomingMessageHeader, txes types.Transactions, hooks *arbos.SequencingHooks) (*types.Block, error) {
    hooks := arbos.NoopSequencingHooks()

    block, receipts, err := arbos.ProduceBlockAdvanced(
        header,
        txes,
        delayedMessagesRead,
        lastBlockHeader,
        statedb,
        s.bc,
        s.bc.Config(),
        hooks,
    )
}

func ProduceBlockAdvanced(
    l1Header *arbostypes.L1IncomingMessageHeader,
    txes types.Transactions,
    delayedMessagesRead uint64,
    lastBlockHeader *types.Header,
    statedb *state.StateDB,
    chainContext core.ChainContext,
    chainConfig *params.ChainConfig,
    sequencingHooks *SequencingHooks,
) (*types.Block, types.Receipts, error) {
    // Prepend a tx before all others to touch up the state (update the L1 block num, pricing pools, etc)
    // 会调用合约
    startTx := InternalTxStartBlock(chainConfig.ChainID, l1Header.L1BaseFee, l1BlockNum, header, lastBlockHeader)
    txes = append(types.Transactions{types.NewTx(startTx)}, txes...)

    for len(txes) > 0 || len(redeems) > 0 {
        // repeatedly process the next tx, doing redeems created along the way in FIFO order

        var tx *types.Transaction
        
        receipt, result, err := (func() (*types.Receipt, *core.ExecutionResult, error) {
            // hooks.PreTxFilter = nil
            if err = hooks.PreTxFilter(chainConfig, header, statedb, state, tx, options, sender, l1Info); err != nil {
                return nil, nil, err
            }

            // extraPreTxFilter = nill
            if err = extraPreTxFilter(chainConfig, header, statedb, state, tx, options, sender, l1Info); err != nil {
                return nil, nil, err
            }

            // 处理交易
            receipt, result, err := core.ApplyTransactionWithResultFilter(
                chainConfig,
                chainContext,
                &header.Coinbase,
                &gasPool,
                statedb,
                header,
                tx,
                &header.GasUsed,
                vm.Config{},
                func(result *core.ExecutionResult) error {
                    return hooks.PostTxFilter(header, state, tx, sender, dataGas, result)
                },
            )

            // extraPostTxFilter = nil
            if err = extraPostTxFilter(chainConfig, header, statedb, state, tx, options, sender, l1Info, result); err != nil {
                statedb.RevertToSnapshot(snap)
                return nil, nil, err
            }

            return receipt, result, nil
        })()
}
func ApplyTransactionWithResultFilter(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, resultFilter func(*ExecutionResult) error) (*types.Receipt, *ExecutionResult, error) {
    msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee)
    if err != nil {
        return nil, nil, err
    }
    // Create a new context to be used in the EVM environment
    blockContext := NewEVMBlockContext(header, bc, author)
    vmenv := vm.NewEVM(blockContext, vm.TxContext{BlobHashes: tx.BlobHashes()}, statedb, config, cfg)
    return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, resultFilter)
}

运行交易

func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, resultFilter func(*ExecutionResult) error) (*types.Receipt, *ExecutionResult, error) {
    // Apply the transaction to the current state (included in the env).
    result, err := ApplyMessage(evm, msg, gp)
}

func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) {
    return NewStateTransition(evm, msg, gp).TransitionDb()
}

func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
    endTxNow, startHookUsedGas, err, returnData := st.evm.ProcessingHook.StartTxHook()

    // First check this message satisfies all consensus rules before
    // applying the message. The rules include these clauses
    //
    // 1. the nonce of the message caller is correct
    // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice)
    // 3. the amount of gas required is available in the block
    // 4. the purchased gas is enough to cover intrinsic usage
    // 5. there is no overflow when calculating intrinsic gas
    // 6. caller has enough balance to cover asset transfer for **topmost** call
    
    // Check clauses 1-3, buy gas if everything is correct
    if err := st.preCheck(); err != nil {
        return nil, err
    }
    
    // Check clauses 4-5, subtract intrinsic gas if everything is correct
    gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
    if err != nil {
        return nil, err
    }
    if st.gasRemaining < gas {
        return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
    }
    st.gasRemaining -= gas
    
    tipReceipient, err := st.evm.ProcessingHook.GasChargingHook(&st.gasRemaining)
    
    if contractCreation {
        deployedContract = &common.Address{}
        ret, *deployedContract, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, msg.Value)
    } else {
        // Increment the nonce for the next transaction
        st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1)
        ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
    }
    
    if !rules.IsLondon {
        // Before EIP-3529: refunds were capped to gasUsed / 2
        st.refundGas(params.RefundQuotient)
    } else {
        // After EIP-3529: refunds are capped to gasUsed / 5
        st.refundGas(params.RefundQuotientEIP3529)
    }
    effectiveTip := msg.GasPrice
    if rules.IsLondon {
        effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee))
    }
    
    if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
        // Skip fee payment when NoBaseFee is set and the fee fields
        // are 0. This avoids a negative effectiveTip being applied to
        // the coinbase when simulating calls.
    } else {
        fee := new(big.Int).SetUint64(st.gasUsed())
        fee.Mul(fee, effectiveTip)
        st.state.AddBalance(tipReceipient, fee)
        tipAmount = fee
    }
    
    st.evm.ProcessingHook.EndTxHook(st.gasRemaining, vmerr == nil)
    
    return &ExecutionResult{
        UsedGas:          st.gasUsed(),
        Err:              vmerr,
        ReturnData:       ret,
        ScheduledTxes:    st.evm.ProcessingHook.ScheduledTxes(),
        TopLevelDeployed: deployedContract,
    }, nil
}

交易 fee

TransactionToMessage() 中创建了 Message 后,修改 msg.GasPrice 为两者中的最小值:最大小费单价 + 基础费;最大手续费单价 msg.GasPrice = cmath.BigMin(msg.GasPrice.Add(msg.GasTipCap, baseFee), msg.GasFeeCap)

func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
    msg := &Message{
        Tx: tx,

        Nonce:             tx.Nonce(),
        GasLimit:          tx.Gas(),
        GasPrice:          new(big.Int).Set(tx.GasPrice()),
        GasFeeCap:         new(big.Int).Set(tx.GasFeeCap()),
        GasTipCap:         new(big.Int).Set(tx.GasTipCap()),
        To:                tx.To(),
        Value:             tx.Value(),
        Data:              tx.Data(),
        AccessList:        tx.AccessList(),
        SkipAccountChecks: tx.SkipAccountChecks(), // TODO Arbitrum upstream this was init'd to false
        BlobHashes:        tx.BlobHashes(),
        BlobGasFeeCap:     tx.BlobGasFeeCap(),
    }
    // If baseFee provided, set gasPrice to effectiveGasPrice.
    if baseFee != nil {
        msg.GasPrice = cmath.BigMin(msg.GasPrice.Add(msg.GasTipCap, baseFee), msg.GasFeeCap)
    }
    var err error
    msg.From, err = types.Sender(s, tx)
    return msg, err
}

TransitionDb() 中有如下逻辑,一般不会走到

// Arbitrum: drop tip for delayed (and old) messages
// 假如 ArbOSVersion 是 11,DropTip() 返回 true
// func (p *TxProcessor) DropTip() bool {
//     version := p.state.ArbOSVersion()
//     return version != 9 || p.delayedInbox
// }
if st.evm.ProcessingHook.DropTip() && st.msg.GasPrice.Cmp(st.evm.Context.BaseFee) > 0 {
    st.msg.GasPrice = st.evm.Context.BaseFee
    st.msg.GasTipCap = common.Big0
}

TransitionDb() -> preCheck() -> buyGas() 中,大多数情况下都从用户的账户中扣除 st.msg.GasLimit * st.msg.GasPrice

func (st *StateTransition) buyGas() error {
    mgval := new(big.Int).SetUint64(st.msg.GasLimit)
    mgval = mgval.Mul(mgval, st.msg.GasPrice)
    balanceCheck := new(big.Int).Set(mgval)
    if st.msg.GasFeeCap != nil {
        balanceCheck.SetUint64(st.msg.GasLimit)
        balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
        balanceCheck.Add(balanceCheck, st.msg.Value)
    }
    if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
        if blobGas := st.blobGasUsed(); blobGas > 0 {
            // Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
            blobBalanceCheck := new(big.Int).SetUint64(blobGas)
            blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap)
            balanceCheck.Add(balanceCheck, blobBalanceCheck)
            // Pay for blobGasUsed * actual blob fee
            blobFee := new(big.Int).SetUint64(blobGas)
            blobFee.Mul(blobFee, eip4844.CalcBlobFee(*st.evm.Context.ExcessBlobGas))
            mgval.Add(mgval, blobFee)
        }
    }
    if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 {
        return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
    }
    if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
        return err
    }
    st.gasRemaining += st.msg.GasLimit

    st.initialGas = st.msg.GasLimit
    st.state.SubBalance(st.msg.From, mgval)

    // Arbitrum: record fee payment
    if tracer := st.evm.Config.Tracer; tracer != nil {
        tracer.CaptureArbitrumTransfer(st.evm, &st.msg.From, nil, mgval, true, "feePayment")
    }

    return nil
}

TransitionDb() 中扣除 intrinsic gas

// Check clauses 4-5, subtract intrinsic gas if everything is correct
gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
if err != nil {
    return nil, err
}
if st.gasRemaining < gas {
    return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
}
st.gasRemaining -= gas

TransitionDb() -> GasChargingHook() 中扣掉 L1 gas,并将 L1 fee 换算为等价的 L2 fee

tipReceipient, err := st.evm.ProcessingHook.GasChargingHook(&st.gasRemaining)
func (p *TxProcessor) GasChargingHook(gasRemaining *uint64) (common.Address, error) {
    // Because a user pays a 1-dimensional gas price, we must re-express poster L1 calldata costs
    // as if the user was buying an equivalent amount of L2 compute gas. This hook determines what
    // that cost looks like, ensuring the user can pay and saving the result for later reference.

    var gasNeededToStartEVM uint64
    tipReceipient, _ := p.state.NetworkFeeAccount()
    basefee := p.evm.Context.BaseFee

    var poster common.Address
    if p.msg.TxRunMode != core.MessageCommitMode {
        poster = l1pricing.BatchPosterAddress
    } else {
        poster = p.evm.Context.Coinbase
    }

    if p.msg.TxRunMode == core.MessageCommitMode {
        p.msg.SkipL1Charging = false
    }
    if basefee.Sign() > 0 && !p.msg.SkipL1Charging {
        // Since tips go to the network, and not to the poster, we use the basefee.
        // Note, this only determines the amount of gas bought, not the price per gas.

        brotliCompressionLevel, err := p.state.BrotliCompressionLevel()
        if err != nil {
            return common.Address{}, fmt.Errorf("failed to get brotli compression level: %w", err)
        }
        posterCost, calldataUnits := p.state.L1PricingState().PosterDataCost(p.msg, poster, brotliCompressionLevel)
        if calldataUnits > 0 {
            p.state.Restrict(p.state.L1PricingState().AddToUnitsSinceUpdate(calldataUnits))
        }
        p.posterGas = GetPosterGas(p.state, basefee, p.msg.TxRunMode, posterCost)
        p.PosterFee = arbmath.BigMulByUint(basefee, p.posterGas) // round down
        gasNeededToStartEVM = p.posterGas
    }

    if *gasRemaining < gasNeededToStartEVM {
        // the user couldn't pay for call data, so give up
        return tipReceipient, core.ErrIntrinsicGas
    }
    *gasRemaining -= gasNeededToStartEVM

    if p.msg.TxRunMode != core.MessageEthcallMode {
        // If this is a real tx, limit the amount of computed based on the gas pool.
        // We do this by charging extra gas, and then refunding it later.
        gasAvailable, _ := p.state.L2PricingState().PerBlockGasLimit()
        if *gasRemaining > gasAvailable {
            p.computeHoldGas = *gasRemaining - gasAvailable
            *gasRemaining = gasAvailable
        }
    }
    return tipReceipient, nil
}

TransitionDb() 中执行 tx

if contractCreation {
    deployedContract = &common.Address{}
    ret, *deployedContract, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, msg.Value)
} else {
    // Increment the nonce for the next transaction
    st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1)
    ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
}

TransitionDb() 中将多扣的 fee 还给用户

if !rules.IsLondon {
    // Before EIP-3529: refunds were capped to gasUsed / 2
    st.refundGas(params.RefundQuotient)
} else {
    // After EIP-3529: refunds are capped to gasUsed / 5
    st.refundGas(params.RefundQuotientEIP3529)
}
func (st *StateTransition) refundGas(refundQuotient uint64) {
    // Return ETH for remaining gas, exchanged at the original rate.
    remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gasRemaining), st.msg.GasPrice)
    st.state.AddBalance(st.msg.From, remaining)
}

TransitionDb() 中将小费 effectiveTip * st.gasUsed() 给 networkFeeAccount

effectiveTip := msg.GasPrice
if rules.IsLondon {
    effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee))
}
    
if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
    // Skip fee payment when NoBaseFee is set and the fee fields
    // are 0. This avoids a negative effectiveTip being applied to
    // the coinbase when simulating calls.
} else {
    fee := new(big.Int).SetUint64(st.gasUsed())
    fee.Mul(fee, effectiveTip)
    st.state.AddBalance(tipReceipient, fee) // tipReceipient = networkFeeAccount
    tipAmount = fee
}

TransitionDb() -> EndTxHook() 中,将 base fee 给 infraFeeAccount 和 l1pricing.L1PricerFundsPoolAddress,如果还有小费,给 networkFeeAccount

func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) {
    basefee := p.evm.Context.BaseFee
    totalCost := arbmath.BigMul(basefee, arbmath.UintToBig(gasUsed)) // total cost = price of gas * gas burnt
    computeCost := arbmath.BigSub(totalCost, p.PosterFee)            // total cost = network's compute + poster's L1 costs
    if computeCost.Sign() < 0 {
        // Uh oh, there's a bug in our charging code.
        // Give all funds to the network account and continue.

        log.Error("total cost < poster cost", "gasUsed", gasUsed, "basefee", basefee, "posterFee", p.PosterFee)
        p.PosterFee = big.NewInt(0)
        computeCost = totalCost
    }

    purpose := "feeCollection"
    if p.state.ArbOSVersion() > 4 {
        infraFeeAccount, err := p.state.InfraFeeAccount()
        p.state.Restrict(err)
        if infraFeeAccount != (common.Address{}) {
            minBaseFee, err := p.state.L2PricingState().MinBaseFeeWei()
            p.state.Restrict(err)
            infraFee := arbmath.BigMin(minBaseFee, basefee)
            computeGas := arbmath.SaturatingUSub(gasUsed, p.posterGas)
            infraComputeCost := arbmath.BigMulByUint(infraFee, computeGas)
            util.MintBalance(&infraFeeAccount, infraComputeCost, p.evm, scenario, purpose)
            computeCost = arbmath.BigSub(computeCost, infraComputeCost)
        }
    }
    if arbmath.BigGreaterThan(computeCost, common.Big0) {
        util.MintBalance(&networkFeeAccount, computeCost, p.evm, scenario, purpose)
    }
    posterFeeDestination := l1pricing.L1PricerFundsPoolAddress
    if p.state.ArbOSVersion() < 2 {
        posterFeeDestination = p.evm.Context.Coinbase
    }
    util.MintBalance(&posterFeeDestination, p.PosterFee, p.evm, scenario, purpose)
    if p.state.ArbOSVersion() >= 10 {
        if _, err := p.state.L1PricingState().AddToL1FeesAvailable(p.PosterFee); err != nil {
            log.Error("failed to update L1FeesAvailable: ", "err", err)
        }
    }

    if p.msg.GasPrice.Sign() > 0 { // in tests, gas price could be 0
        // ArbOS's gas pool is meant to enforce the computational speed-limit.
        // We don't want to remove from the pool the poster's L1 costs (as expressed in L2 gas in this func)
        // Hence, we deduct the previously saved poster L2-gas-equivalent to reveal the compute-only gas

        var computeGas uint64
        if gasUsed > p.posterGas {
            // Don't include posterGas in computeGas as it doesn't represent processing time.
            computeGas = gasUsed - p.posterGas
        } else {
            // Somehow, the core message transition succeeded, but we didn't burn the posterGas.
            // An invariant was violated. To be safe, subtract the entire gas used from the gas pool.
            log.Error("total gas used < poster gas component", "gasUsed", gasUsed, "posterGas", p.posterGas)
            computeGas = gasUsed
        }
        p.state.Restrict(p.state.L2PricingState().AddToGasPool(-arbmath.SaturatingCast(computeGas)))
    }
}

参考

https://blog-blockchain.xyz/geth/understand-tx/index.html