About the EVM Part II

jerichou
2025-05-22 / 0 评论 / 4 阅读 / 正在检测是否收录...

8.控制流(Control Flow)指令

以太坊虚拟机(EVM)中的控制流指令用于控制程序执行的流程。这类指令不涉及算术计算或内存操作,而是改变程序计数器(Program Counter,pc)的值,从而跳转到新的位置继续执行。

以下是常用的 5 个控制流指令及其详细说明(包含源码级别原理和 Gas 消耗):


🛑 1. STOP(0x00)
功能:

终止当前执行,正常返回。

源码实现:
func opStop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    return nil, errStopToken
}
行为:
  • 不消耗任何栈、内存等资源。
  • 直接停止程序执行。
Gas 消耗:
  • 0 Gas

🔁 2. JUMP(0x56)
功能:

跳转到某个位置执行。

前提:

跳转目标必须是 JUMPDEST 指令的地址,否则异常。

栈行为:
栈顶:跳转地址
源码实现:
func opJump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    if interpreter.evm.abort.Load() {
        return nil, errStopToken
    }
    pos := scope.Stack.pop()
    if !scope.Contract.validJumpdest(&pos) {
        return nil, ErrInvalidJump
    }
    *pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop
    return nil, nil
}
  • validJumpdest 会检查该地址是否是 JUMPDEST(0x5b)字节。
  • 由于 pc 在每条指令之后自动加 1,所以此处设置为 dest - 1
Gas 消耗:
  • 8 Gas

❓ 3. JUMPI(0x57)
功能:

条件跳转,只有条件非零才跳转。

栈行为:
栈顶:condition
次栈顶:destination
源码实现:
func opJumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    dest, cond := scope.Stack.pop(), scope.Stack.pop()

    if cond.IsZero() {
        return nil, nil // 不跳转
    }
    if !interpreter.validJumpdest(dest.Uint64(), scope.Contract.Code) {
        return nil, ErrInvalidJump
    }
    *pc = dest.Uint64() - 1
    return nil, nil
}
行为说明:
  • 如果 cond == 0,什么也不做。
  • 如果 cond != 0,执行跳转到 dest
Gas 消耗:
  • 10 Gas

🎯 4. JUMPDEST(0x5b)
功能:

跳转目标的标记,标志合法的 JUMP/JUMPI 可跳转的位置。

源码实现:
func opJumpdest(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    return nil, nil
}
行为:
  • 什么都不做,就是个占位符。
  • 编译器在生成代码时会在跳转目标位置插入 JUMPDEST
Gas 消耗:
  • 1 Gas

📍 5. PC(0x58)
功能:

将当前指令的程序计数器 pc 压入栈。

栈行为:
push 当前 PC
源码实现:
func opPc(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).SetUint64(*pc))
    return nil, nil
}
用途:
  • 常用于合约内部的程序构造(如 inline jump)。
  • JUMP 配合使用时可构造动态跳转。
Gas 消耗:
  • 2 Gas

🔍 总结对比表:

指令含义栈操作Gas 消耗说明
STOP停止执行0不返回值,正常终止
JUMP跳转弹出目标地址8目标地址必须是 JUMPDEST
JUMPI条件跳转弹出地址和条件10条件非零跳转
JUMPDEST跳转目标标记1合法跳转的目标
PC获取当前指令地址压入当前 pc2可用于构造 jump 表

9.查询区块信息

以太坊虚拟机(EVM)中用于查询区块信息的 9 个指令的详细说明,包括其功能、Gas 消耗以及简要的源码实现原理。


📦 区块信息相关指令一览

指令名操作码功能描述Gas 消耗栈操作(输入 → 输出)
BLOCKHASH0x40获取指定区块号的哈希(最近 256 个区块)20blockNumberblockHash
COINBASE0x41当前区块的矿工地址2address
TIMESTAMP0x42当前区块的时间戳2timestamp
NUMBER0x43当前区块号2blockNumber
PREVRANDAO0x44上一个区块的随机数(Beacon 链)2randomness
GASLIMIT0x45当前区块的 Gas 上限2gasLimit
CHAINID0x46当前链的 Chain ID2chainId
BASEFEE0x48当前区块的 Base Fee2baseFee
BLOBBASEFEE0x4A当前区块的 Blob Base Fee(EIP-7516)2blobBaseFee

🔍 指令详解与源码实现概述

1. BLOCKHASH(0x40)
  • 功能:获取指定区块号的哈希,仅限于最近 256 个区块。
  • Gas 消耗:20
  • 源码实现概述

    • 从栈中弹出区块号。
    • 调用 GetHash(blockNumber) 方法获取对应的区块哈希。
    • 将结果压入栈中。
2. COINBASE(0x41)
  • 功能:返回当前区块的矿工地址。
  • Gas 消耗:2
  • 源码实现概述

    • 直接从区块上下文中读取 Coinbase 字段。
    • 将结果压入栈中。
3. TIMESTAMP(0x42)
  • 功能:返回当前区块的时间戳。
  • Gas 消耗:2
  • 源码实现概述

4. NUMBER(0x43)
  • 功能:返回当前区块号。
  • Gas 消耗:2
  • 源码实现概述

    • 从区块上下文中读取 Number 字段。
    • 将结果压入栈中。
5. PREVRANDAO(0x44)
  • 功能:返回上一个区块的随机数(适用于以太坊 2.0)。
  • Gas 消耗:2
  • 源码实现概述

    • 从区块上下文中读取 PrevRandao 字段。
    • 将结果压入栈中。
6. GASLIMIT(0x45)
  • 功能:返回当前区块的 Gas 上限。
  • Gas 消耗:2
  • 源码实现概述

7. CHAINID(0x46)
  • 功能:返回当前链的 Chain ID。
  • Gas 消耗:2
  • 源码实现概述

8. BASEFEE(0x48)
9. BLOBBASEFEE(0x4A)
  • 功能:返回当前区块的 Blob Base Fee(EIP-7516)。
  • Gas 消耗:2
  • 源码实现概述

10.账户 Account 相关

EVM 中与 账户 Account 相关的 4 个指令分别是:

  • BALANCE (0x31)
  • EXTCODESIZE (0x3B)
  • EXTCODECOPY (0x3C)
  • EXTCODEHASH (0x3F)

在以太坊的 Geth 实现中,每个账户(外部账户或合约账户)在执行期间被建模为一个 stateObject,保存在 StateDB.stateObjects 这个 map 中,其 key 是账户地址,value 就是 *stateObject

🔍 stateObject 结构字段详解

👉 基本信息字段
字段类型说明
db*StateDB关联的状态数据库,提供持久化和状态读取功能
addresscommon.Address当前账户地址(20 字节)
addrHashcommon.Hash当前地址的 keccak256 哈希,作为 MPT 中的键
origin*types.StateAccount初始状态副本(还没被当前 block 修改的快照)
datatypes.StateAccount当前状态数据,包含变更后余额、nonce、storage root、code hash

其中,types.StateAccount 是这样定义的:

type StateAccount struct {
    Nonce    uint64
    Balance  *uint256.Int
    Root     common.Hash // storage trie 的根哈希
    CodeHash []byte      // 合约字节码的哈希值
}

👉 存储相关字段(Storage Tries)

以太坊合约的每个 storage 都是一个单独的 Merkle Patricia Trie。

字段类型说明
trieTrie 接口当前账户的 storage trie(延迟加载,首次访问时才初始化)
originStorageStorage(map[Hash]Hash)当前区块中曾读取过的存储项,原始值
dirtyStorageStorage当前交易中被写入的存储项(临时更改)
pendingStorageStorage当前区块中写入但尚未 commit 的存储项
uncommittedStorageStorageByzantium 之后,追踪自 block 开始以来的所有修改(辅助合约重入和 Gas Refund)

这些字段的存在是为了:

  • 加速访问:减少 trie 查询频率
  • 支持事务隔离:交易提交前不影响其他交易
  • 支持 fork 条件下状态回滚与恢复
⚠️ 特别注意:只有 commit 之后,才会写入 storage trie,并最终持久化到 disk 上的 LevelDB。

👉 合约代码缓存相关

字段类型说明
code[]byte缓存的合约字节码(由 CodeHash 指向)
dirtyCodebool合约代码是否在当前交易中被修改过(用于判断是否需要更新 CodeHash)
  • 合约代码是不可变的,因此通常只在合约部署时写入一次。
  • 读取时会从 CodeHash 中查找缓存或 LevelDB 中的代码。

👉 Self-Destruct 与合约创建状态

字段类型说明
selfDestructedbool是否在当前交易中触发了 SELFDESTRUCT,但还没彻底清理
newContractbool是否是新部署的合约(用于 EIP-6780 控制 selfdestruct 规则)

selfDestructed 为 true 的账户在交易内仍然可访问,但交易结束后会被移除(余额归属给指定地址)。

newContract 是为了解决 EIP-6780 提出的新规则 —— 并非所有合约都能 selfdestruct。


📌 stateObject 的作用概括

作用说明
状态建模描述一个账户的完整状态(余额、nonce、storage、code)
执行隔离支持事务级别的变更记录、撤销和写入
持久化桥梁从内存(变更)到 MPT(trie 节点)再到 LevelDB
Gas 计算通过状态变更辅助计算 Gas(如 storage 修改、code 更新)
Fork 支持支持区块级与交易级的状态快照、回滚和恢复

🧠 额外说明:Storage 的生命周期

阶段操作写入位置
读取SLOADoriginStorage
写入SSTOREdirtyStorage(交易级)→ pendingStorage(区块级)
提交tx commitpendingStorage 提交到 storage trie(trie
区块 commitblock commit把所有账户的 trie 根更新到 global state trie 的节点

下面我将分别给出它们在 Go-Ethereum 中的源码实现(关键代码段)、功能、Gas 消耗和行为解释。


✅ 1. BALANCE (0x31)

📌 功能

返回指定地址的账户余额(以 wei 为单位)

⛽ Gas 消耗
  • Cold access: 2600(EIP-2929)
  • Warm access: 100
🧠 Go 源码
func opBalance(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    slot := scope.Stack.peek()
    address := common.Address(slot.Bytes20())
    slot.Set(interpreter.evm.StateDB.GetBalance(address))
    return nil, nil
}

func (s *StateDB) GetBalance(addr common.Address) *uint256.Int {
    stateObject := s.getStateObject(addr)
    if stateObject != nil {
        return stateObject.Balance()
    }
    return common.U2560
}
func (s *stateObject) Balance() *uint256.Int {
    return s.data.Balance
}
type stateObject struct {
    db       *StateDB
    address  common.Address      // address of ethereum account
    addrHash common.Hash         // hash of ethereum address of the account
    origin   *types.StateAccount // Account original data without any change applied, nil means it was not existent
    data     types.StateAccount  // Account data with all mutations applied in the scope of block
...
}

type StateAccount struct {
    Nonce    uint64
    Balance  *uint256.Int
    Root     common.Hash // merkle root of the storage trie
    CodeHash []byte
}
🔍 说明
  • scope.Stack 是当前 EVM 执行上下文的栈。
  • peek() 获取栈顶元素(并不弹出)。
  • 通过 Bytes20() 将其强制转换成 20 字节(以太坊地址)。然后包装为 common.Address
  • getStateObject 会尝试从状态树或缓存中获取地址对应的账户(stateObject)。

    • 如果存在,调用 Balance()
    • 如果地址不存在(合约或账户从未出现过),则余额视为 0(common.U2560uint256 的 0 值)。
  • stateObject.data 是当前账户的状态快照(类型 StateAccount)。

    其中 Balance 是个 *uint256.Int 指针,存储账户余额。


✅ 2. EXTCODESIZE (0x3B)

📌 功能

返回某个地址的合约字节码长度

⛽ Gas 消耗
  • Cold access: 2600(EIP-2929)
  • Warm access: 100
🧠 Go 源码
func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    slot := scope.Stack.peek()
    slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20())))
    return nil, nil
}
🔍 说明
  • StateDB.GetCodeSize(addr) 获取代码字节数组长度

✅ 3. EXTCODECOPY (0x3C)

📌 功能

将指定地址的合约代码复制到内存中(用于读取部分代码)

⛽ Gas 消耗
  • Base: 100
  • Per word copied: 3 × ceil(size / 32)
  • Cold access penalty: +2600(如果是冷访问)
🧠 Go 源码
func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
   var (
        memOffset  = scope.Stack.pop()
        codeOffset = scope.Stack.pop()
        length     = scope.Stack.pop()
    )
    uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
    if overflow {
        uint64CodeOffset = math.MaxUint64
    }

    codeCopy := getData(scope.Contract.Code, uint64CodeOffset, length.Uint64())
    scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
    return nil, nil
}
🔍 说明
  • 从 EVM 操作数栈中依次弹出:

    • memOffset: 写入内存的起始偏移位置
    • codeOffset: 要拷贝的代码段的起始位置
    • length: 要拷贝的字节数
  • 代码偏移量从大整数转换为 uint64,若超出范围就使用最大值。防止恶意代码传入极大值导致 panic。
  • 从目标账户的代码中提取指定区间的字节(类似切片截取)
  • 如果越界了,EVM 要求 padding 为 0(填零)。append(..., make(...)) 是为了避免 panic 并补足返回值
  • 用于代理合约(Proxy)判断目标地址是否有代码。

    • 用于合约间交互时动态读取合约逻辑。
    • 用于合约内通过代码检查是否是合约地址(如 EIP-1167 的 minimal proxy)。

✅ 4. EXTCODEHASH (0x3F)

📌 功能

返回指定地址代码的 keccak256(code),如果该地址没有代码,则返回 0

⛽ Gas 消耗
  • Cold access: 2600(EIP-1052)
  • Warm access: 100
🧠 Go 源码
func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    slot := scope.Stack.peek()
    address := common.Address(slot.Bytes20())
    if interpreter.evm.StateDB.Empty(address) {
        slot.Clear()
    } else {
        slot.SetBytes(interpreter.evm.StateDB.GetCodeHash(address).Bytes())
    }
    return nil, nil
}
🔍 说明
  • StateDB 获取该账户的 代码哈希(通常是 Keccak256),并设置到栈顶。

📊 总结对比表

指令功能Gas(Cold/Warm)依赖 StateDB 方法
BALANCE账户余额2600 / 100GetBalance, AccessAccount
EXTCODESIZE合约代码长度2600 / 100GetCode, AccessAccount
EXTCODECOPY复制合约代码到内存2600 + 3/wordGetCode, AccessAccount
EXTCODEHASH合约代码哈希2600 / 100GetCodeHash, HasCode

11.交易相关

EVM 中与 交易上下文(Transaction Context) 相关的 4 个核心指令包括:

指令功能
ADDRESS当前合约地址
ORIGIN外部交易发起方地址(EOA)
CALLER当前调用者地址
CALLVALUE调用时附带的 msg.value 数值

下面将详细从 最新 Geth 源码(v1.14.x) 层面解析这些指令的逻辑实现,以及它们的 Gas 消耗


🔹 1. ADDRESS

✅ 功能

返回当前执行合约的地址(address(this)

📘 源码实现
func opAddress(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Address().Bytes()))
    return nil, nil
}

解释

  • scope.Contract 是当前正在执行的合约实例。
  • Address() 返回其地址,20 字节压入栈顶。
⛽ Gas 消耗
操作Gas
ADDRESS2

🔹 2. ORIGIN

✅ 功能

返回最初交易发起者地址(一个 EOA),即 tx.origin

📘 源码实现
func opOrigin(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).SetBytes(interpreter.evm.Origin.Bytes()))
    return nil, nil
}

解释

  • interpreter.evm.Origin 在执行上下文初始化时设置。
  • 通常为最初的 EOA(Externally Owned Account)地址。
⛽ Gas 消耗
操作Gas
ORIGIN2

🔹 3. CALLER

✅ 功能

返回当前消息调用者(可以是合约或EOA),即 msg.sender

📘 源码实现
func opCaller(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Caller().Bytes()))
    return nil, nil
}

解释

  • 如果合约 A 调用合约 B,则 msg.sender 为 A。
  • scope.Contract.Caller() 即当前调用者地址。
⛽ Gas 消耗
操作Gas
CALLER2

🔹 4. CALLVALUE

✅ 功能

返回当前调用时附带的以 wei 为单位的金额,即 msg.value

📘 源码实现
func opCallValue(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).Set(scope.Contract.value))
    return nil, nil
}

解释

  • scope.Contract.value 表示此次消息调用的 ETH 附带值。
  • 是一个 *uint256.Int 表示的数值。
⛽ Gas 消耗
操作Gas
CALLVALUE2

✅ 汇总对比表格

指令功能栈操作来源字段Gas
ADDRESS当前合约地址push(address)scope.Contract.Address()2
ORIGIN交易原始发起者(EOA)push(tx.origin)interpreter.evm.Origin2
CALLER调用者地址(msg.sender)push(msg.sender)scope.Contract.Caller()2
CALLVALUE附带 ETH 值(msg.value)push(msg.value)scope.Contract.value2

12.日志相关

EVM 中与日志(Log)相关的指令是:

指令名说明
LOG0不带 topic 的日志
LOG11 个 topic
LOG22 个 topic
LOG33 个 topic
LOG44 个 topic(最多)

这些指令由智能合约调用 LOGx 指令时触发,最终写入 TransactionReceipt 的 logs 字段中。


📦 Golang 源码位置

路径:core/vm/instructions.go

LOG0 为例,实际实现是共用一个函数:

func makeLog(size int) executionFunc {
    return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
        if interpreter.readOnly {
            return nil, ErrWriteProtection
        }
        topics := make([]common.Hash, size)
        stack := scope.Stack
        mStart, mSize := stack.pop(), stack.pop()
        for i := 0; i < size; i++ {
            addr := stack.pop()
            topics[i] = addr.Bytes32()
        }

        d := scope.Memory.GetCopy(mStart.Uint64(), mSize.Uint64())
        interpreter.evm.StateDB.AddLog(&types.Log{
            Address: scope.Contract.Address(),
            Topics:  topics,
            Data:    d,
            // This is a non-consensus field, but assigned here because
            // core/state doesn't know the current block number.
            BlockNumber: interpreter.evm.Context.BlockNumber.Uint64(),
        })

        return nil, nil
    }
}

在初始化指令集时,会这样注册:

        LOG0: {
            execute:    makeLog(0),
            dynamicGas: makeGasLog(0),
            minStack:   minStack(2, 0),
            maxStack:   maxStack(2, 0),
            memorySize: memoryLog,
        },
        LOG1: {
            execute:    makeLog(1),
            dynamicGas: makeGasLog(1),
            minStack:   minStack(3, 0),
            maxStack:   maxStack(3, 0),
            memorySize: memoryLog,
        },
        LOG2: {
            execute:    makeLog(2),
            dynamicGas: makeGasLog(2),
            minStack:   minStack(4, 0),
            maxStack:   maxStack(4, 0),
            memorySize: memoryLog,
        },
        LOG3: {
            execute:    makeLog(3),
            dynamicGas: makeGasLog(3),
            minStack:   minStack(5, 0),
            maxStack:   maxStack(5, 0),
            memorySize: memoryLog,
        },
        LOG4: {
            execute:    makeLog(4),
            dynamicGas: makeGasLog(4),
            minStack:   minStack(6, 0),
            maxStack:   maxStack(6, 0),
            memorySize: memoryLog,
        },

🔍 详细解析
mStart, mSize

从合约内存中提取数据的起始位置和长度。

topics

根据 LOGx 中的 x,从栈中弹出对应数量的 topic

data

从合约内存中 GetCopy 取出 mSize 字节数据,这就是事件的 data 部分。

❹ 调用 StateDB.AddLog

这会将日志加入 logs 中,最终附着在交易回执(TransactionReceipt)中,供事件监听器或前端读取。


⛽ Gas 消耗

params\protocol_params.go 中定义:

LogGas                uint64 = 375   // Per LOG* operation.
🔢 LOGx 的 Gas 消耗计算方式:
Gas = memoryGasCost(mem, memorySize) + 375(基础) + 375 * topic 数 + 8 * data 长度(按字节)

举例:

  • LOG2,有 2 个 topic,日志数据长 100 字节:

    Gas = memoryGasCost(mem, memorySize) + 375 + 375×2 + 8×100 = 375 + 750 + 800 = 1925

✅ 总结表格

指令Topics 数栈项数功能Gas 构成
LOG002写入 datamemoryGasCost(mem, memorySize) + 375 + 8×dataLen
LOG113写入 1 topic + datamemoryGasCost(mem, memorySize) + 375 + 375×1 + 8×dataLen
LOG224写入 2 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×2 + 8×dataLen
LOG335写入 3 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×3 + 8×dataLen
LOG446写入 4 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×4 + 8×dataLen

🧠 实战补充

合约中通常使用:

event Transfer(address indexed from, address indexed to, uint256 amount);

emit Transfer(msg.sender, to, value);

EVM 将此编译为 LOG3(事件签名 hash + 2 indexed topics)。使用LOG3是因为函数选择器会被默认作为一个topic

LOG0 的事件是无法被事件过滤器(filter)快速索引的,记录不需要索引的调试信息或原始数据

事件声明indexed 数量EVM LOG 指令实际 topic 数量(包含 selector)
event RawData(bytes data) anonymous;0LOG00
event A(uint256 value);0LOG11(只有事件 selector)
event B(address indexed from);1LOG22(selector + from)
event C(address indexed a, address indexed b);2LOG33(selector + a + b)
event D(a indexed, b indexed, c indexed);3LOG44(selector + a + b + c)

📌 布隆过滤器在 Ethereum 中的作用

🔍 主要用于:加速事件日志的检索

当你在使用 Web3.js、ethers.js 或者通过区块浏览器(例如 Etherscan)查询事件时,底层会:

  • 查询区块头中的布隆过滤器(logsBloom 字段)。
  • 检查事件的 topic 是否可能出现在该区块中。

如果布隆过滤器中没命中,就一定没有这个事件,就不用再查日志详情了;
如果命中了,再去读取区块中完整的日志,逐条精确匹配。


🧠 举个例子

你查:

contract.filters.Transfer(address1, null)

想查找某个地址是否发送过 Transfer 事件。

  • 查询每个区块的 logsBloom:

    • 如果 logsBloom 中没有这个地址(作为 topic 编码),那就不用继续查这个区块了
    • 如果有,再去深入查 logs(日志项),看有没有匹配的 Transfer 事件。

📦 EVM 的布隆过滤器结构

每个区块头中有个字段:logsBloom,大小是:

  • 256 字节 = 2048 bit
  • 为每个日志的 topic 和地址计算哈希(Keccak256),提取若干 bit 位置标记为 1

布隆过滤器构造时大致流程(简化):

bloom := [256]byte{} // 2048 bits

// 对每个 topic/address 做 keccak256,提取 3 个位置置 1
positions := getBloomBits(keccak256(data)) 
for pos in positions {
    bloom[pos] = 1
}

✅ 总结:Ethereum 中日志用布隆过滤器的目的

功能描述
🌱 过滤加速快速跳过不包含目标事件的区块,节省读取时间
⚡ 快速索引只要 logsBloom 未命中,就无需解析区块体
📊 控制开销减少节点为每个请求都加载全量日志的成本
🧪 概率保障不会漏查存在的事件,但可能多查几个误判的区块(少量)
2

评论 (0)

取消