首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
34
篇与
的结果
2025-05-21
About the EVM Part I
一、EVM 工作原理概述EVM 是一个 基于栈的图灵完备虚拟机,每次交易或合约调用都会在 EVM 中创建一个执行上下文,用于处理指令(opcode)。基本流程:接收交易:用户发起交易或合约调用,EVM 收到包含输入数据和 gas 的执行请求。读取字节码:合约代码会被编译成字节码(bytecode),并由 EVM 逐条解释执行。执行字节码:字节码指令在 EVM 中执行,通过操作栈、内存、存储、程序计数器(PC)等来完成逻辑。消耗 gas:每执行一条指令都会消耗 gas,gas 不足会导致回滚。更新状态:执行结束后,将结果写入状态树(State Trie)或回滚。二、EVM 中的三种关键存储结构1. 堆栈(Stack)大小固定:最大深度为 1024。操作方式:后进先出(LIFO),类似于汇编里的寄存器。所有算术逻辑操作都必须通过堆栈完成,如 ADD, MUL, CALLDATALOAD 等。只能操作栈顶元素(通过 PUSH, POP, DUP, SWAP 控制)。示例:PUSH1 0x02 PUSH1 0x03 ADD执行后:堆栈中留下 0x05。2. 内存(Memory)临时存储空间:生命周期只在当前合约调用过程中,调用结束就释放。是一个线性字节数组,按需扩展,初始化全为 0。可读写,适合处理中间数据,如函数参数、返回值、字符串、数组等。操作指令包括:MLOAD, MSTORE, MSTORE8。成本: 访问越高地址,gas 越贵(因为内存要扩容)。3. 存储(Storage)永久存储空间:存储在区块链状态中,合约变量的最终值都保存在这里。结构类似一个巨大的 key-value 映射(256-bit 到 256-bit)。极其昂贵(读比内存贵很多,写更贵),所以要谨慎使用。操作指令包括:SLOAD, SSTORE。示例: Solidity 中的 uint a; 在 EVM 中会映射到 storage[0],a = 5 相当于 SSTORE 0 5。三、EVM 执行模型简图:四、执行环境(Execution Context)Program Counter(PC):指向当前执行的字节码位置。Gas:限制执行资源,防止死循环。Call Data:外部调用传入的数据,只读。Code:当前执行的合约字节码。Logs:用于触发事件。返回值区域:放置 RETURN 指令返回的数据。五、与 Solidity 的映射关系Solidity 结构EVM 存储位置局部变量Stack / Memory状态变量Storage函数参数Memory / Stack数组 / 映射Storage / Memory(视情况而定)六、Opcodes1.分类Opcodes可以根据功能分为以下几类:堆栈(Stack)指令: 这些指令直接操作EVM堆栈。这包括将元素压入堆栈(如PUSH1)和从堆栈中弹出元素(如POP)。算术(Arithmetic)指令: 这些指令用于在EVM中执行基本的数学运算,如加法(ADD)、减法(SUB)、乘法(MUL)和除法(DIV)。比较(Comparison)指令: 这些指令用于比较堆栈顶部的两个元素。例如,大于(GT)和小于(LT)。位运算(Bitwise)指令: 这些指令用于在位级别上操作数据。例如,按位与(AND)和按位或(OR)。内存(Memory)指令: 这些指令用于操作EVM的内存。例如,将内存中的数据读取到堆栈(MLOAD)和将堆栈中的数据存储到内存(MSTORE)。存储(Storage)指令: 这些指令用于操作EVM的账户存储。例如,将存储中的数据读取到堆栈(SLOAD)和将堆栈中的数据保存到存储(SSTORE)。这类指令的gas消耗比内存指令要大。控制流(Control Flow)指令: 这些指令用于EVM的控制流操作,比如跳转JUMP和跳转目标JUMPDEST。上下文(Context)指令: 这些指令用于获取交易和区块的上下文信息。例如,获取msg.sender(CALLER)和当前可用的gas(GAS)。2.堆栈(Stack)指令栈结构的定义路径:go-ethereum/core/vm/stack.go//栈结构是一个uint256的切片 type Stack struct { data []uint256.Int }push确保了切片的长度最多1024func (st *Stack) push(d *uint256.Int) { // NOTE push limit (1024) is checked in baseCheck st.data = append(st.data, *d) } func (st *Stack) pop() (ret uint256.Int) { ret = st.data[len(st.data)-1] st.data = st.data[:len(st.data)-1] return }PUSH 指令族:PUSH1 ~ PUSH32作用:将后面紧跟的 1~32 字节常量压入栈顶。✅ 举例:PUSH1 0x60 // 相当于把十六进制 0x60 推入栈顶 PUSH2 0x1234 // 把 0x1234(2字节)推入栈顶源码实现位置:makePush 函数是 go-ethereum 中 EVM 对 PUSH1~PUSH32 指令族的统一处理函数,作用是生成一个具体的 PUSHn 执行函数。size:表示这个操作码整体长度是多少个字节(即 1 + pushByteSize,1 字节的 opcode + n 字节数据)pushByteSize:要从字节码中读取的字节数(即 PUSHn 中的 n)该函数返回一个 executionFunc,这个函数会在 EVM 执行该指令时被调用。🔍 函数体详解:func makePush(size uint64, pushByteSize int) executionFunc { return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( codeLen = len(scope.Contract.Code) start = min(codeLen, int(*pc+1)) end = min(codeLen, start+pushByteSize) ) a := new(uint256.Int).SetBytes(scope.Contract.Code[start:end]) // Missing bytes: pushByteSize - len(pushData) if missing := pushByteSize - (end - start); missing > 0 { a.Lsh(a, uint(8*missing)) } scope.Stack.push(a) *pc += size return nil, nil } }Step 1️⃣ — 获取 PUSH 数据的范围codeLen := len(scope.Contract.Code) start := min(codeLen, int(*pc+1)) // 第一个立即数字节位置 end := min(codeLen, start+pushByteSize) // 最后一个字节的位置*pc 当前指向 PUSHn 操作码(1 字节)所以 *pc+1 才是数据开始的地址因为 EVM 执行过程中可能读取到结尾,需要用 min 防止越界🧠 例子:假设字节码为:0x60 0x0A // PUSH1 0x0A则:*pc = 0,opcode = 0x60 (PUSH1)start = 1end = 2scope.Contract.Code[1:2] = [0x0A]Step 2️⃣ — 转为 uint256.Inta := new(uint256.Int).SetBytes(scope.Contract.Code[start:end])将 start:end 范围内的字节转换为一个 256-bit 整数(高位补 0)。Step 3️⃣ — 补齐不足的位数(重要)if missing := pushByteSize - (end - start); missing > 0 { a.Lsh(a, uint(8*missing)) // 左移补0 }为什么要这么处理?因为如果字节码写错、数据不足,比如字节码是 0x62 0xFF(本来是 PUSH2 0xFF??,但只写了一个字节),那读取结果是:pushByteSize = 2实际读取到的只有 1 字节所以我们就需要左移 8 位(1 字节 = 8 bits)补 0,相当于将 0xFF 变成 0xFF00这是 符合 EVM 规范的行为。Step 4️⃣ — 压入栈scope.Stack.push(a)Step 5️⃣ — 更新程序计数器*pc += sizesize = 1 + pushByteSize,跳过当前 opcode 及其立即数。DUP 指令族:DUP1 ~ DUP16作用:将栈顶向下第 N 个元素复制一份,压到栈顶。例如:stack: [0x01 0x02 0x03] // 栈顶是 0x03 执行 DUP2 后 stack: [0x01 0x02 0x03 0x02] // 把第 2 个 0x02 复制到栈顶源码实现:func makeDup(size int64) executionFunc { return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.dup(int(size)) return nil, nil } }SWAP 指令族:SWAP1 ~ SWAP16作用:将栈顶元素与第 N 个元素交换。例如:stack: [0x01 0x02 0x03] // 栈顶是 0x03 执行 SWAP2 后 stack: [0x03 0x02 0x01]源码实现:// core\vm\instructions.go func opSwap1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.swap1() return nil, nil } // core\vm\stack.go func (st *Stack) swap1() { st.data[st.len()-2], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-2] }Gas 消耗指令Gas CostPOP2PUSHn3DUPn3SWAPn33.算术(Arithmetic)指令EVM 中的 ADD、MUL、SUB、DIV 是最基本的 4 个算术操作指令,它们都以 栈操作 的方式工作 —— 取出操作数、计算结果、再压回栈顶。EVM 中除了最基本的 ADD、MUL、SUB、DIV 运算指令外,还支持各种运算操作,包括 模运算、位运算、比较运算、溢出安全运算、签名数运算 等等源码的实现中做了些优化, 少一次 push 少一次 popx := scope.Stack.pop()弹出栈顶(操作数1)y := scope.Stack.peek()获取栈顶的下一个元素(操作数2),但不弹出y.Add(&x, y)用 x + y 的结果就地更新 y 所在的栈位置✅ 也就是说,它相当于 y = x + y,结果直接写回原来的 y 的位置。func opAdd(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Add(&x, y) return nil, nil } func opSub(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Sub(&x, y) return nil, nil } func opMul(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Mul(&x, y) return nil, nil } func opDiv(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Div(&x, y) ⛽ Gas 消耗对比(EIP-150 后常规执行成本):指令操作码GasADD0x013MUL0x025SUB0x033DIV0x0454.比较(Comparison)指令比较的指令实现也比较简单 从栈中弹出两个操作数 x 和 y,比较它们的大小,并将结果(1 或 0)写回栈顶。然后做了优化操作数y只取出值然后将比较的结果覆盖yfunc opLt(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() if x.Lt(y) { y.SetOne() } else { y.Clear() } return nil, nil } func opGt(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() if x.Gt(y) { y.SetOne() } else { y.Clear() } return nil, nil } func opSlt(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() if x.Slt(y) { y.SetOne() } else { y.Clear() } return nil, nil } func opSgt(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() if x.Sgt(y) { y.SetOne() } else { y.Clear() } return nil, nil } func opEq(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() if x.Eq(y) { y.SetOne() } else { y.Clear() } return nil, nil } func opIszero(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x := scope.Stack.peek() if x.IsZero() { x.SetOne() } else { x.Clear() } return nil, nil }🔍 比较运算指令概览指令名操作码描述操作数结果Gas 消耗LT0x10无符号小于比较a, ba < b3GT0x11无符号大于比较a, ba > b3SLT0x12有符号小于比较a, ba < b3SGT0x13有符号大于比较a, ba > b3EQ0x14等于比较a, ba == b3ISZERO0x15判断是否为零aa == 035.位运算(Bitwise)指令🔧 8 个位级运算指令及其含义指令操作码含义AND0x16位与OR0x17位或XOR0x18位异或NOT0x19位取反BYTE0x1A提取字节SHL0x1B左移(无符号)SHR0x1C右移(无符号)SAR0x1D右移(有符号)📁对应源码实现(go-ethereum)以 go-ethereum 为例,所有这些位运算函数都定义在:core/vm/instructions.gofunc opAnd(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.And(&x, y) return nil, nil } func opOr(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Or(&x, y) return nil, nil } func opXor(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y := scope.Stack.pop(), scope.Stack.peek() y.Xor(&x, y) return nil, nil } func opByte(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { th, val := scope.Stack.pop(), scope.Stack.peek() val.Byte(&th) return nil, nil } func opAddmod(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek() z.AddMod(&x, &y, z) return nil, nil } func opMulmod(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek() z.MulMod(&x, &y, z) return nil, nil } // opSHL implements Shift Left // The SHL instruction (shift left) pops 2 values from the stack, first arg1 and then arg2, // and pushes on the stack arg2 shifted to the left by arg1 number of bits. func opSHL(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { // Note, second operand is left in the stack; accumulate result into it, and no need to push it afterwards shift, value := scope.Stack.pop(), scope.Stack.peek() if shift.LtUint64(256) { value.Lsh(value, uint(shift.Uint64())) } else { value.Clear() } return nil, nil } // opSHR implements Logical Shift Right // The SHR instruction (logical shift right) pops 2 values from the stack, first arg1 and then arg2, // and pushes on the stack arg2 shifted to the right by arg1 number of bits with zero fill. func opSHR(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { // Note, second operand is left in the stack; accumulate result into it, and no need to push it afterwards shift, value := scope.Stack.pop(), scope.Stack.peek() if shift.LtUint64(256) { value.Rsh(value, uint(shift.Uint64())) } else { value.Clear() } return nil, nil } // opSAR implements Arithmetic Shift Right // The SAR instruction (arithmetic shift right) pops 2 values from the stack, first arg1 and then arg2, // and pushes on the stack arg2 shifted to the right by arg1 number of bits with sign extension. func opSAR(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { shift, value := scope.Stack.pop(), scope.Stack.peek() if shift.GtUint64(256) { if value.Sign() >= 0 { value.Clear() } else { // Max negative shift: all bits set value.SetAllOne() } return nil, nil } n := uint(shift.Uint64()) value.SRsh(value, n) return nil, nil } ⛽ 三、Gas 消耗(来自 gas_table.go)大部分位运算的 Gas 是 固定的低成本操作(3 gas),属于非常轻量的计算。指令Gas 成本说明AND3params.VeryLowGasOR3 XOR3 NOT3实际在 EIP-145 前不支持BYTE3提取某个字节SHL3EIP-145: 位移操作SHR3 SAR3 6.内存(Memory)指令在 EVM 中,内存操作指令主要有以下 4 个:指令含义操作码MSTORE写入 32 字节0x52MSTORE8写入 1 字节0x53MLOAD读取 32 字节0x51MSIZE查询当前内存大小0x59🔧 源码实现(来自 Geth)➡️core\vm\memory.gomemory对象实现的方式type Memory struct { store []byte lastGasCost uint64 }EVM 的 Memory 是一个临时的、线性地址空间的内存,用于执行合约时的数据操作(比如 MSTORE, MLOAD 等)。它是一个自动扩容的 []byte 数组:store []byte:存储所有内存数据。lastGasCost:记录上一次内存扩容所花费的 gas(用于避免重复计算 gas)。♻️ 内存池管理(内存复用)var memoryPool = sync.Pool{ New: func() any { return &Memory{} }, }为什么用 sync.Pool?内存对象会被频繁创建和销毁(每个合约执行都需要一块 Memory),直接频繁分配和回收会增加 GC 压力。用 sync.Pool 可以避免重复申请,提高性能。func NewMemory() *Memory { return memoryPool.Get().(*Memory) } go复制编辑func (m *Memory) Free() { const maxBufferSize = 16 << 10 // 最大 16KB if cap(m.store) <= maxBufferSize { m.store = m.store[:0] m.lastGasCost = 0 memoryPool.Put(m) } }只回收「不大」的内存(防止泄漏大对象)。重置后放回池中。🧱 内存写入:Set, Set32Set(offset, size, value)func (m *Memory) Set(offset, size uint64, value []byte)写入从 offset 开始、size 大小的数据。要求必须在写入前先调用 Resize 否则 panic。用于 MSTORE, MSTORE8 等指令。Set32(offset, val)func (m *Memory) Set32(offset uint64, val *uint256.Int)写入一个 32 字节整型值(如 uint256)。使用 val.PutUint256(m.store[offset:]) 写入 32 字节。用于 MSTORE 指令。📐 内存读取:GetCopy, GetPtrGetCopy(offset, size) []byte返回内存中 [offset, offset+size) 的 拷贝副本。cpy := make([]byte, size) copy(cpy, m.store[offset:offset+size])安全:防止外部修改原始内存。GetPtr(offset, size) []byte返回原始 store 的切片指针(没有拷贝,性能高)。return m.store[offset : offset+size]🪜Resize:内存扩容func (m *Memory) Resize(size uint64)如果现有 store 长度不足,就扩容。实际通过 append(make([]byte, size-delta)) 实现。Go 的 append 虽然可以触发扩容,但这是不可控的自动行为。EVM 需要 开发者主动指定目标大小每次 Memory 扩容都需要支付额外 gas,以防止攻击者用 memory 拖垮执行资源。memory 中未写入的部分默认值是 0。⚠️ 所有读写操作前都 必须手动调用 Resize,否则会 panic!🧪 Copyfunc (m *Memory) Copy(dst, src, len uint64)将内存从 src 拷贝 len 字节到 dst。可能会发生重叠(支持 memmove)。⚠️ 不会自动扩容,调用前需确保 Resize 过。📊 辅助方法func (m *Memory) Len() int func (m *Memory) Data() []byteLen():当前内存长度(不是容量)。Data():返回整个 store。⚙️使用示例(例如 MSTORE)EVM 执行 MSTORE:// 假设 offset = 0x40,val = 0xdeadbeef... memory.Resize(offset + 32) // 先扩容 memory.Set32(offset, val) // 写入32字节⛽ Gas 计费:由内存增长触发EVM 的内存使用不是免费:每 32 字节为单位扩容时需要支付 gas,如下(在调用 Resize 的地方):memoryGasCost = newMemorySizeWords^2 / 512 - oldSize^2 / 512这也是为什么 lastGasCost 字段存在——为了记住上一次内存扩容的 gas 开销,避免重复计算。➡️ core/vm/instructions.go✅ 1. MSTORE — 写入 32 字节(256bit)func opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { offset, val := scope.Stack.pop(), scope.Stack.pop() interpreter.memory.Set32(offset.Uint64(), &val) return nil, nil }说明:从栈中取出 offset 和 val,将 val 写入到 memory[offset : offset+32]函数内部调用:memory.Set32(off, val) 是内部封装的 32 字节写入函数✅ 2. MSTORE8 — 写入 1 字节(最低位)func opMstore8(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { off, val := scope.Stack.pop(), scope.Stack.pop() scope.Memory.store[off.Uint64()] = byte(val.Uint64()) return nil, nil }说明:将 val 的最低有效字节(byte 31)写入到内存 offset 处,适合 byte 操作Byte(31) 取的是最后一个字节(高位在前,符合 big endian)✅ 3. MLOAD — 从内存读取 32 字节func opMload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { v := scope.Stack.peek() offset := v.Uint64() v.SetBytes(scope.Memory.GetPtr(offset, 32)) return nil, nil }说明:从 memory[offset : offset+32] 读取 32 字节,并 push 到栈中✅ 4. MSIZE — 当前内存大小(以字节为单位)func opMsize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(interpreter.memory)))) return nil, nil }说明:直接把当前 memory 长度 push 到栈顶,单位是 字节⛽ Gas 消耗(来自 gas_table.go)🧮 Gas 成本相关规则:指令基础 Gas 消耗说明MLOAD3VeryLow(不包括内存扩展成本)MSTORE3同上MSTORE83同上MSIZE2BaseGas⏫ 内存扩展依照扩展字节数,增加额外 Gas,见 memoryGasCost 逻辑 示例片段:VeryLowGas = 3 BaseGas = 2 ... {Op: MLOAD, Gas: VeryLowGas, Exec: opMload}, {Op: MSTORE, Gas: VeryLowGas, Exec: opMstore}, {Op: MSTORE8, Gas: VeryLowGas, Exec: opMstore8}, {Op: MSIZE, Gas: BaseGas, Exec: opMsize},此外,EVM 内存是动态扩展的,每次写入可能导致内存增长,增长的部分会按比例增加 gas 消耗(在 memoryGasCost.go 中实现)。📘 总结对比指令栈操作内存操作Gas 消耗MSTOREpop(offset), pop(value)memory[offset : offset+32] ← value3 + mem扩展MSTORE8pop(offset), pop(value)memory[offset] ← value[31]3 + mem扩展MLOADpop(offset) → push(value)value ← memory[offset : offset+32]3 + mem扩展MSIZEpush(memory size)无直接操作27.存储(Storage)指令在 EVM 中,存储(Storage) 是合约的持久化数据存储区域,数据在交易结束后仍然保留在链上。两条指令用于操作存储:SLOAD: 加载一个存储槽的数据SSTORE: 将一个值写入存储槽🔧 源码级别的实现原理📥 SLOAD 实现从合约状态中读取指定位置的数据:func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { loc := scope.Stack.peek() // 读取存储槽索引 hash := common.Hash(loc.Bytes32()) // 转为 32 字节 hash 作为 slot key val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash) loc.SetBytes(val.Bytes()) // 设置读取值到栈顶(栈中原来的 slot 被改为值) return nil, nil }📌 实现要点:栈顶元素是要读取的 slot 索引(比如 slot 0x00, 0x01...)合约地址 + slot 构成唯一的 key,访问 StateDB修改原始 slot 元素值为读取结果(避免 pop 再 push)📤 SSTORE 实现将一个值写入合约状态:func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { addr := scope.Contract.Address() loc := scope.Stack.pop() // slot val := scope.Stack.pop() // value key := common.Hash(loc.Bytes32()) // 设置 storage 新值(实际存储延迟提交) interpreter.evm.StateDB.SetState(addr, key, val.Bytes32()) return nil, nil }📌 实现要点:两个参数:slot 和 value,顺序是 value 在上,slot 在下StateDB.SetState 会记录这个 slot 的修改(可以用于追踪变更)实际写入状态树会在交易完成后统一提交💰Gas 消耗逻辑(重点)✅ 1. SLOAD gas 消耗固定费用:100 gas不考虑新旧值关系,直接从状态树加载 slot 值GasSload = 100✅ 2. SSTORE gas 消耗(复杂)根据旧值 / 新值 / 初始值不同,收费策略不同。规则详解(London 升级后 EIP-3529):情况Gas 消耗说明把非 0 改为 05000,并 返还 4800 gas"清除" 存储,EVM 喜欢把 0 改为非 020000增加状态大小,最贵把非 0 改为非 0(值变了)5000一般修改操作把值改为原始值(回滚)100无实际变更(无效写)原始值指合约开始执行时 slot 的值(用于判断是否真正变更)💡 状态修改追踪: StateDBEVM 中通过 StateDB 记录:originalValue:交易开始时的状态dirtyValue:当前修改但未提交的值这让 SSTORE 能判断:if new == old { // 无修改 } else if original == old { // 真正变更,走 expensive path } else { // 被多次修改,不重复计费 }在 EVM(以太坊虚拟机)中,StateDB 是管理合约账户、合约代码和存储(Storage)的核心组件,承担着读写全局状态的职责。我们来从源码和功能层面详细拆解 StateDB 的实现:在以太坊中,每个账户有:账户余额(balance)随机数(nonce)合约代码(如果是合约账户)存储(键值对:key => value)StateDB 提供了统一的接口来读取和写入这些数据,并在交易结束时统一提交或丢弃状态变化。StateDB 的关键接口和结构体StateDB 接口定义go复制编辑type StateDB interface { GetBalance(addr common.Address) *big.Int GetNonce(addr common.Address) uint64 GetCode(addr common.Address) []byte GetCodeSize(addr common.Address) int GetCodeHash(addr common.Address) common.Hash GetState(addr common.Address, key common.Hash) common.Hash SetState(addr common.Address, key, value common.Hash) AddBalance(addr common.Address, amount *big.Int) SubBalance(addr common.Address, amount *big.Int) SetNonce(addr common.Address, nonce uint64) SetCode(addr common.Address, code []byte) Suicide(addr common.Address) bool HasSuicided(addr common.Address) bool Snapshot() int RevertToSnapshot(int) Commit(deleteEmptyObjects bool) error }这表示它支持账户基本信息的读写、合约状态管理、回滚和提交等。实现结构体 stateObject每个账户在 StateDB 中以 stateObject 的形式存在:type stateObject struct { address common.Address balance *big.Int nonce uint64 code []byte storage Trie dirtyStorage map[common.Hash]common.Hash ... }内部管理一份账户的状态,包括 storage Trie(Merkle Patricia Trie 结构),并记录脏数据用于延迟写入。Storage 的底层结构:Merkle Patricia Trie(MPT)每个合约的 storage 是一个 MPT。它的特点:可验证(Merkle 结构)支持路径压缩(Patricia)Key 为 32 字节哈希,Value 为 32 字节哈希更新存储时,EVM 会记录 dirty 数据,等交易提交时统一将这些数据写入 MPT 中,并生成新的 stateRoot。🚀 总结对比指令类型功能Gas 消耗是否持久SLOAD读读取 slot 值100 gas否SSTORE写设置 slot 值100 ~ 20,000 gas(看旧值)是(写入区块链)
2025年05月21日
8 阅读
0 评论
1 点赞
2025-05-16
Uniswap v2
{collapse}{collapse-item label="一、背景" open}传统的中心化交易所买家和卖家进行订单匹配,去中心的交易所引入开创了一种全新的交易方式,通过一种预设的数学公式(比如,常数乘积公式)创建一个流动性池,使得用户可以随时进行交易。自动做市商(Automated Market Maker,简称 AMM)是一种算法,或者说是一种在区块链上运行的智能合约,它允许数字资产之间的去中心化交易。{/collapse-item}{collapse-item label="二、常见的AMM算法" open} ## 1. 恒定乘积做市商(Constant Product Market Maker, CPMM)公式x * y = kx:代币A的数量y:代币B的数量k:恒定乘积(流动性池的总价值)特点代表协议:Uniswap V1/V2滑点机制:交易量越大,价格偏离越多(非线性)。流动性分布:全价格区间均匀分布,资本效率低。2. 恒定和做市商(Constant Sum Market Maker, CSMM)公式x + y = k特点零滑点:价格固定为 1:1(如稳定币交易)。缺点:流动性易耗尽(套利者会抽干某一代币)。代表协议:早期稳定币兑换池(已淘汰)。示例池中有 100 DAI + 100 USDC,用户用 10 DAI 换 10 USDC,池变为 110 DAI + 90 USDC。3. 恒定均值做市商(Constant Mean Market Maker, CMMM)公式x^a y^b z^c = ka,b,c:代币权重(可自定义)。特点支持多代币池(如Balancer的3种代币池)。灵活权重:例如 80% ETH + 20% BTC 的指数基金池。代表协议:Balancer。4. 集中流动性AMM(Concentrated Liquidity)代表协议:Uniswap V3原理LP可自定义价格区间(如 ETH/USDC 在 $1800-$2200 提供流动性)。公式分段处理:在区间内:使用 CPMM(x * y = k)。在区间外:流动性失效(变为单一资产)。优势资本效率提升:最高达4000倍(相比V2)。支持限价单:通过流动性区间模拟。5. 动态费用AMM(Dynamic Fees)代表协议:Trader Joe(Liquidity Book)特点根据市场波动动态调整手续费(如高波动时提高费率)。分档流动性:将价格划分为多个“档位”,每档独立计算流动性。AMM算法的核心问题与解决方案问题解决方案协议示例资本效率低集中流动性(Uniswap V3)Uniswap V3无常损失动态费率或对冲策略Bancor V3多代币池支持恒定均值算法(Balancer)Balancer总结CPMM(Uniswap):简单通用,适合大部分代币对。CSMM:仅适合稳定币,已淘汰。CMMM(Balancer):灵活支持多代币和自定义权重。集中流动性(Uniswap V3):提升资本效率,适合专业做市。{/collapse-item}{collapse-item label="三、实现一个简单的swap" open} SimpleSwap 继承了 ERC20 代币标准,方便记录流动性提供者提供的流动性。在构造器中,我们指定一对代币地址 token0 和 token1,交易所仅支持这对代币。reserve0 和 reserve1 记录了合约中代币的储备量。contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 构造器,初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } }交易所主要有两类参与者:流动性提供者(Liquidity Provider,LP)和交易者(Trader)。下面我们分别实现这两部分的功能。流动性提供流动性提供者给市场提供流动性,让交易者获得更好的报价和流动性,并收取一定费用。首先,我们需要实现添加流动性的功能。当用户向代币池添加流动性时,合约要记录添加的LP份额。这个LP份额主要是用户提供流动性的凭证,交易费的一定比例按 LP 份额分配给流动性提供者,同时也可以赎回代币,销毁 LP 代币,按LP比例取回 tokenA 和 tokenB,具体的tokenA和tokenB数量取决于他们现有数量然后乘LP比例。根据 Uniswap V2,LP份额如下计算:Δx 和 Δy 分别表示一笔交易中token和美元的变化量代币池被首次添加流动性时,LP份额 ΔL 由添加代币数量乘积的平方根决定:非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个):因为 SimpleSwap 合约继承了 ERC20 代币标准,在计算好LP份额后,可以将份额以代币形式铸造LP代币给用户。下面的 addLiquidity() 函数实现了添加流动性的功能,主要步骤如下:将用户添加的代币转入合约,需要用户事先给合约授权。根据公式计算添加的流动性份额,并检查铸造的LP数量。更新合约的代币储备量。给流动性提供者铸造LP代币。释放 Mint 事件。event Mint(address indexed sender, uint amount0, uint amount1); // 添加流动性,转进代币,铸造LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约,需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造liquidity个LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); }接下来,我们需要实现移除流动性的功能。当用户从池子中移除流动性 ΔL 时,合约要销毁LP份额代币,并按比例将代币返还给用户。返还代币的计算公式如下:下面的 removeLiquidity() 函数实现移除流动性的功能,主要步骤如下:获取合约中的代币余额。按LP的比例计算要转出的代币数量。检查代币数量。销毁LP份额。将相应的代币转账给用户。更新储备量。释放 Burn 事件。// 移除流动性,销毁LP,转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); }至此,合约中与流动性提供者相关的功能完成了,接下来是交易的部分。交易在Swap合约中,用户可以使用一种代币交易另一种。那么我用 Δx单位的 token0,可以交换多少单位的 token1 呢?下面我们来简单推导一下。根据恒定乘积公式,交易前:交易后,有:交易前后 k 值不变,联立上面等式,可以得到:因此,可以交换到的代币数量 Δy 由 Δx,x,和 y 决定。注意 Δx 和 Δy 的符号相反,因为转入会增加代币储备量,而转出会减少。下面的 getAmountOut() 实现了给定一个资产的数量和代币对的储备,计算交换另一个代币的数量。// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); }有了这一核心公式后,我们可以着手实现交易功能了。下面的 swap() 函数实现了交易代币的功能,主要步骤如下:用户在调用函数时指定用于交换的代币数量,交换的代币地址,以及换出另一种代币的最低数量。判断是 token0 交换 token1,还是 token1 交换 token0。利用上面的公式,计算交换出代币的数量。判断交换出的代币是否达到了用户指定的最低数量,这里类似于交易的滑点。将用户的代币转入合约。将交换的代币从合约转给用户。更新合约的代币储备量。释放 Swap 事件。// swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); }Swap 合约SimpleSwap 的完整代码如下:// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 事件 event Mint(address indexed sender, uint amount0, uint amount1); event Burn(address indexed sender, uint amount0, uint amount1); event Swap( address indexed sender, uint amountIn, address tokenIn, uint amountOut, address tokenOut ); // 构造器,初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } // 取两个数的最小值 function min(uint x, uint y) internal pure returns (uint z) { z = x < y ? x : y; } // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) function sqrt(uint y) internal pure returns (uint z) { if (y > 3) { z = y; uint x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } // 添加流动性,转进代币,铸造LP // 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1) // 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约,需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); } // 移除流动性,销毁LP,转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); } // 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 // 由于乘积恒定 // 交换前: k = x * y // 交换后: k = (x + delta_x) * (y + delta_y) // 可得 delta_y = - delta_x * y / (x + delta_x) // 正/负号代表转入/转出 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); } // swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); } }1.部署两个ERC20合约token0和token12.部署 SimpleSwap 合约,并将token0和token1的地址作为构造函数参数3.调用两个ERC20代币的approve()函数,分别给 SimpleSwap 合约授权 1000 单位代币。4.调用 SimpleSwap 合约的 addLiquidity() 函数给交易所添加流动性,token0 和 token1 分别添加 100 单位。5.调用 SimpleSwap 合约的 balanceOf() 函数查看用户的LP份额,这里应该为 100。6.调用 SimpleSwap 合约的 swap() 函数进行代币交易,用 100 单位的 token07.调用 SimpleSwap 合约的 reserve0 和 reserve1 函数查看合约中的代币储备粮,应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1{/collapse-item}{/collapse}
2025年05月16日
5 阅读
0 评论
1 点赞
2025-05-14
ERC20 Contract
{collapse}{collapse-item label="一、定义" open}ERC20ERC20是以太坊上的代币标准,来自2015年11月V神参与的EIP20。它实现了代币转账的基本逻辑:账户余额(balanceOf())转账(transfer())授权转账(transferFrom())授权(approve())代币总供给(totalSupply())授权转账额度(allowance())代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals())IERC20IERC20是ERC20代币标准的接口合约,规定了ERC20代币需要实现的函数和事件。 之所以需要定义接口,是因为有了规范后,就存在所有的ERC20代币都通用的函数名称,输入参数,输出参数。 在接口函数中,只需要定义函数名称,输入参数,输出参数,并不关心函数内部如何实现。 由此,函数就分为内部和外部两个内容,一个重点是实现,另一个是对外接口,约定共同数据。 这就是为什么需要ERC20.sol和IERC20.sol两个文件实现一个合约。事件IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放/** * @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时. */ event Approval(address indexed owner, address indexed spender, uint256 value);函数IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。totalSupply()返回代币总供给/** * @dev 返回代币总供给. */ function totalSupply() external view returns (uint256);balanceOf()返回账户余额/** * @dev 返回账户`account`所持有的代币数. */ function balanceOf(address account) external view returns (uint256);transfer()转账/** * @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`. * * 如果成功,返回 `true`. * * 释放 {Transfer} 事件. */ function transfer(address to, uint256 amount) external returns (bool);allowance()返回授权额度/** * @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。 * * 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变. */ function allowance(address owner, address spender) external view returns (uint256);approve()授权/** * @dev 调用者账户给`spender`账户授权 `amount`数量代币。 * * 如果成功,返回 `true`. * * 释放 {Approval} 事件. */ function approve(address spender, uint256 amount) external returns (bool);transferFrom()授权转账/** * @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。 * * 如果成功,返回 `true`. * * 释放 {Transfer} 事件. */ function transferFrom( address from, address to, uint256 amount ) external returns (bool);实现ERC20现在我们写一个ERC20,将IERC20规定的函数简单实现。状态变量我们需要状态变量来记录账户余额,授权额度和代币信息。其中balanceOf, allowance和totalSupply为public类型,会自动生成一个同名getter函数,实现IERC20规定的balanceOf(), allowance()和totalSupply()。而name, symbol, decimals则对应代币的名称,代号和小数位数。注意:用override修饰public变量,会重写继承自父合约的与变量同名的getter函数,比如IERC20中的balanceOf()函数。mapping(address => uint256) public override balanceOf; mapping(address => mapping(address => uint256)) public override allowance; uint256 public override totalSupply; // 代币总供给 string public name; // 名称 string public symbol; // 代号 uint8 public decimals = 18; // 小数位数函数构造函数:初始化代币名称、代号。constructor(string memory name_, string memory symbol_){ name = name_; symbol = symbol_; }transfer()函数:实现IERC20中的transfer函数,代币转账逻辑。调用方扣除amount数量代币,接收方增加相应代币。土狗币会魔改这个函数,加入税收、分红、抽奖等逻辑。function transfer(address recipient, uint amount) public override returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[recipient] += amount; emit Transfer(msg.sender, recipient, amount); return true; }approve()函数:实现IERC20中的approve函数,代币授权逻辑。被授权方spender可以支配授权方的amount数量的代币。spender可以是EOA账户,也可以是合约账户:当你用uniswap交易代币时,你需要将代币授权给uniswap合约。function approve(address spender, uint amount) public override returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; }transferFrom()函数:实现IERC20中的transferFrom函数,授权转账逻辑。被授权方将授权方sender的amount数量的代币转账给接收方recipient。function transferFrom( address sender, address recipient, uint amount ) public override returns (bool) { allowance[sender][msg.sender] -= amount; balanceOf[sender] -= amount; balanceOf[recipient] += amount; emit Transfer(sender, recipient, amount); return true; }mint()函数:铸造代币函数,不在IERC20标准中。这里为了教程方便,任何人可以铸造任意数量的代币,实际应用中会加权限管理,只有owner可以铸造代币:function mint(uint amount) external { balanceOf[msg.sender] += amount; totalSupply += amount; emit Transfer(address(0), msg.sender, amount); }burn()函数:销毁代币函数,不在IERC20标准中。function burn(uint amount) external { balanceOf[msg.sender] -= amount; totalSupply -= amount; emit Transfer(msg.sender, address(0), amount); }{/collapse-item}{collapse-item label="二、应用" open}1. 代币水龙头代币水龙头就是让用户免费领代币的网站/应用。实现一个简版的ERC20水龙头,逻辑非常简单:我们将一些ERC20代币转到水龙头合约里,用户可以通过合约的requestToken()函数来领取100单位的代币,每个地址只能领一次。状态变量我们在水龙头合约中定义3个状态变量amountAllowed设定每次能领取代币数量(默认为100,不是一百枚,因为代币有小数位数)。tokenContract记录发放的ERC20代币合约地址。requestedAddress记录领取过代币的地址。uint256 public amountAllowed = 100; // 每次领 100 单位代币 address public tokenContract; // token合约地址 mapping(address => bool) public requestedAddress; // 记录领取过代币的地址事件水龙头合约中定义了1个SendToken事件,记录了每次领取代币的地址和数量,在requestTokens()函数被调用时释放。// SendToken事件 event SendToken(address indexed Receiver, uint256 indexed Amount); 函数合约中只有两个函数:构造函数:初始化tokenContract状态变量,确定发放的ERC20代币地址。// 部署时设定ERC20代币合约 constructor(address _tokenContract) { tokenContract = _tokenContract; // set token contract }requestTokens()函数,用户调用它可以领取ERC20代币。// 用户领取代币函数 function requestTokens() external { require(!requestedAddress[msg.sender], "Can't Request Multiple Times!"); // 每个地址只能领一次 IERC20 token = IERC20(tokenContract); // 创建IERC20合约对象 require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了 token.transfer(msg.sender, amountAllowed); // 发送token requestedAddress[msg.sender] = true; // 记录领取地址 emit SendToken(msg.sender, amountAllowed); // 释放SendToken事件 }在Remix上测试(1).先将自定义的dog合约部署查看日志部署成功 生成合约地址(2).使用mint铸造1000个dog到自己钱包中查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址(3).选择水龙头合约部署 填写dog代币合约地址作为水龙头合约构造函数参数查看日志合约部署成功 生成合约地址(4).调用dog合约的transfer发送200个dog给水龙头合约查看日志钱包转了200个dog到水龙头合约地址(5).现在使用一个钱包和水龙头合约进行交互获取dog钱包0x6......7f2收到了100个dog查询dog合约看下钱包0x6......7f2拥有100个dog代币2. Airdrop由于通常接收空投的用户数量较多,项目方逐一发送转账并不实际。通过使用智能合约批量发送ERC20代币,可以显著提高空投的效率。空投代币合约空投合约的逻辑很简单:通过循环,单笔交易将ERC20代币发送到多个地址。合约包含2以下函数:函数getSum():返回数组的总和uint。// sum function for arrays function getSum(uint256[] calldata _arr) public pure returns(uint sum) { for(uint i = 0; i < _arr.length; i++) sum = sum + _arr[i]; }功能multiTransferToken():发送代币空投ERC20,包含3参数:_token:代币合约地址(address类型)_addresses:接收空投的用户地址数组(address[]类型)_amounts_addresses:(uint[]类型)中每个地址对应空投金额数组该函数包含2以下检查:第一个require检查数组的长度是否_addresses等于数组的长度_amounts。第二个require检查空投合约的授权限额是否大于要空投的代币总量。/// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use /// /// @param _token The address of ERC20 token for transfer /// @param _addresses The array of airdrop addresses /// @param _amounts The array of amount of tokens (airdrop amount for each address) function multiTransferToken( address _token, address[] calldata _addresses, uint256[] calldata _amounts ) external { // Check: The length of _addresses array should be equal to the length of _amounts array require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); IERC20 token = IERC20(_token); // Declare IERC contract variable uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); // for loop, use transferFrom function to send airdrops for (uint8 i; i < _addresses.length; i++) { token.transferFrom(msg.sender, _addresses[i], _amounts[i]); } }multiTransferETH()功能:发送ETH空投,2参数:_addresses:接收空投的用户地址数组(address[]类型)_amounts:空投金额数组,对应_addresses(uint[]类型)中每个地址的数量/// Transfer ETH to multiple addresses function multiTransferETH( address payable[] calldata _addresses, uint256[] calldata _amounts ) public payable { // Check: _addresses and _amounts arrays should have the same length require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); // Calculate total amount of ETH to be airdropped uint _amountSum = getSum(_amounts); // Check: transferred ETH should equal total amount require(msg.value == _amountSum, "Transfer amount error"); // Use a for loop to transfer ETH using transfer function for (uint256 i = 0; i < _addresses.length; i++) { _addresses[i].transfer(_amounts[i]); } }在Remix上测试(1).先将自定义的dog合约部署查看日志部署成功 生成合约地址(2).使用mint铸造1000个dog到自己钱包中查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址(3).选择airdrop合约部署查看日志airdrop合约部署成功 生成合约地址(4).使用dog合约的approve函数授权airdrop合约拥有其中800个dog的权限查看日志触发approve事件 授权成功 授权airdrop合约操作钱包地址的800个dog(5).现在管理员就可以通过和airdrop合约交互 指定要发放空投的钱包地址对应的dog个数查看日志钱包被空投了dog(6)拿其中一个钱包去dog合约地址调用balanceof查询钱包的dog个数是100和空投的数量一致{/collapse-item}{/collapse}
2025年05月14日
4 阅读
1 评论
1 点赞
2025-05-14
Blockchain Fundamentals Summary
一、区块链1.定义对区块链最好的描述是将其描述为一个公共数据库,它由网络中的许多计算机更新和共享。"区块"指的是数据和状态是按顺序批量或"区块"存储的。"链"指的是每个区块加密引用其父块。 换句话说,区块被链接在一起。 在不改变所有后续区块的情况下,区块内数据是无法改变,但改变后续区块需要整个网络的共识。网络中的每台计算机都必须就每个新区块和链达成一致。 这些计算机被称为“节点”。 节点保证所有与区块链交互的人都有相同的数据。 要完成此分布式协议,区块链需要一个共识机制。以太坊采用pos共识机制。 任何想在链上添加新区块的人都必须质押以太币(以太坊原生货币)做为抵押品并运行验证者软件。 接着,可以随机选择这些“验证者”来提出区块,再由其他验证者检查并添加到区块链中。 存在一种奖励和惩罚体系,有力地激励参与者尽可能地诚实和保持在线。2.以太的pos和pow联系2.1. 基本机制维度PoW(比特币、早期以太坊)PoS(以太坊2.0、Cardano)验证方式矿工通过算力竞争解决数学难题(哈希碰撞)验证者通过质押代币获得出块权(随机选择)资源消耗依赖高能耗硬件(ASIC/GPU),电力成本极高仅需普通服务器,能耗极低(约为PoW的0.1%)出块速度比特币:10分钟/区块;以太坊PoW:15秒/区块以太坊PoS:12秒/区块(更稳定)2.2. 安全性对比维度PoWPoS攻击成本需控制51%算力(硬件+电力成本)需控制2/3质押代币(经济成本更高)防御手段算力竞争使攻击代价高昂Slashing罚没机制(作恶者质押代币被销毁)分叉风险临时分叉常见(需等待多个确认)几乎无分叉(快速最终性)2.3. 经济模型维度PoWPoS参与者矿工(投入硬件和电力)质押者(锁定代币)收益来源区块奖励+交易费质押奖励+交易费中心化风险矿池集中化(少数矿池控制多数算力)富者愈富(大质押者收益更高)2.4. 去中心化程度维度PoWPoS参与门槛高(需专业矿机)较低(普通用户可质押)硬件依赖ASIC/GPU垄断,普通用户难参与无需特殊硬件,节点易部署治理权力算力决定话语权质押代币量决定话语权2.5. 典型应用PoW:比特币(BTC)、莱特币(LTC)、早期以太坊(ETH 1.0)。PoS:以太坊2.0(ETH)、Cardano(ADA)、Solana(SOL)。2.6.为什么以太坊从PoW转向PoS?能源效率:PoS能耗仅为PoW的0.1%,更环保。安全性:PoS的Slashing机制使攻击成本远高于PoW。可扩展性:PoS为分片链(Sharding)等扩容方案铺路。3.以太坊(ETH)还为网络提供加密经济安全,主要通过以下三种方式实现:奖励机制:用于激励验证者——既奖励正常提议区块的验证者,也奖励揭发其他验证者不诚实行为的验证者;质押抵押:验证者需要质押ETH作为担保,一旦发现恶意行为,其质押的ETH将被销毁;共识权重:在新区块的"投票"过程中,ETH持有量决定投票权重,直接影响共识机制中分叉选择算法的决策。4.EVM(Ethereum Virtual Machine) EVM是一个去中心化的虚拟执行环境,其核心特征包括:全局一致性1.所有以太坊节点以完全相同的逻辑执行代码2.确保全网状态的一致性智能合约执行层1.作为智能合约的专用运行时环境 2.节点通过运行EVM处理合约调用请求Gas计量体系1.采用"gas"作为计算资源计量单位 2.精确量化每项操作的计算成本 3.实现双重保障: ✓ 网络资源高效分配 ✓ 基础层安全防护4.1.EVM的本质EVM(Ethereum Virtual Machine) 是以太坊的运行时环境,专门用于执行智能合约的字节码(EVM Code)。类比eBPF:相似点:两者都是基于字节码的虚拟机,使用JIT(即时编译)技术优化执行。不同点:特性EVMeBPF应用场景执行智能合约(去中心化)内核数据处理(网络/安全)运行环境区块链节点(全球分布式)Linux内核(单机)权限控制无特权限制(Gas费约束)受内核严格沙盒限制JIT优化有(如Ethereum JIT)有(如LLVM JIT)EVM Code(字节码)智能合约的编译结果:开发者用Solidity/Vyper编写合约,编译为EVM字节码。// Solidity源码 contract Example { function add(uint a, uint b) public pure returns (uint) { return a + b; } }→ 编译为EVM字节码(十六进制):0x6080604052348015600f57600080fd5b506004361060285760003560e01c806...执行过程:EVM解析字节码,按指令逐条执行。使用栈(Stack)和内存(Memory)临时存储数据(见下文)。4.2. EVM的栈(Stack)与内存(Memory)(1)EVM栈作用:用于临时存储计算中间值(类似CPU寄存器)。特点:深度固定:最多1024个槽位(每个槽位32字节)。操作受限:只能通过PUSH/POP/SWAP等指令访问栈顶。示例:// Solidity中的加法操作 → EVM字节码 add(a, b) → PUSH1 a, PUSH1 b, ADD栈状态变化:| Stack | | |-------|-------| | | | ← 初始空栈 | a | | ← PUSH1 a | b | a | ← PUSH1 b | a+b | | ← ADD(弹出a和b,压入结果)(2)EVM内存作用:临时存储复杂数据(如数组、字符串)。特点:动态扩容:按需扩展,但需支付Gas费。易失性:仅在当前合约调用期间存在,调用结束后清空。示例:function storeData() public { bytes memory data = new bytes(100); // 分配100字节内存 }→ EVM会分配内存并记录偏移量供后续使用。4.3. 智能合约与EVM的关系(1)智能合约的生命周期编写:用Solidity/Vyper等语言编写逻辑。编译:生成EVM字节码和ABI(应用二进制接口)。部署:将字节码作为交易发送到链上,存储在合约地址中。调用:用户通过交易触发合约函数,EVM执行对应字节码。(2)合约与EVM的交互每个合约调用:创建一个独立的EVM实例,隔离执行环境。Gas机制:限制计算和存储资源,防止无限循环或滥用。4.4. 质押节点与EVM的关系(1)质押节点的组成以太坊PoS节点分为两部分:执行层(Execution Layer):运行EVM,处理交易和智能合约(如Geth、Nethermind客户端)。共识层(Consensus Layer):负责PoS共识(如Prysm、Lighthouse客户端),验证区块和投票。(2)质押者的角色成为验证者:需质押32 ETH并运行执行层+共识层客户端。职责:区块提议者:打包交易并生成新区块(需执行EVM)。投票验证者:对其他区块进行签名确认(不直接涉及EVM)。(3)EVM的分布性每个节点都有EVM:所有完整节点(包括质押节点)本地运行EVM,独立验证交易和合约。全局一致性:所有节点的EVM必须对同一交易输出相同结果(确定性执行)。二、Smart Contracts1. 智能合约的本质智能合约是存储在以太坊区块链上的程序,由代码(函数)和数据(状态)组成,具有以下核心特性:链上账户:拥有独立的地址(如 0x742...d35),可以持有ETH余额并接收交易。不可篡改:一旦部署,代码和状态无法被修改(除非合约自带自毁逻辑)。自动执行:通过交易触发,严格按代码逻辑运行,无需第三方干预。2. 智能合约 vs. 普通账户特性智能合约账户普通用户账户(EOA)控制者代码逻辑私钥持有者创建方式通过交易部署合约代码私钥生成地址触发行为需外部交易调用函数可主动发起交易代码存储有(EVM字节码)无3. 智能合约的核心组成部分(1)代码(Functions)可调用函数:定义合约的行为逻辑(如转账、铸造NFT、投票等)。function mintNFT(address to, uint256 tokenId) public { _mint(to, tokenId); // 内部实现NFT铸造 }(2)数据(State)状态变量:存储在链上的持久化数据(如余额、所有权记录)。mapping(uint256 => address) private _owners; // 记录NFT所有者 uint256 public totalSupply; // NFT总供应量(3)事件(Events)日志记录:用于前端监听合约状态变化(如NFT转账)。event Transfer(address indexed from, address indexed to, uint256 tokenId);4. 智能合约的生命周期编写:用Solidity/Vyper等语言定义逻辑。编译:生成EVM字节码和ABI(应用二进制接口)。部署:通过交易将字节码发送到区块链,生成合约地址。交互:用户或合约调用其函数(如mintNFT)。5. 智能合约的不可逆性代码不可变:默认无法升级或删除(除非代码中包含selfdestruct)。交易不可逆:一旦执行(如NFT铸造),状态变更永久记录在链上。三、密码学基础1.数字签名Alice想证明某个消息是自己发送的Alice用自己的私钥签名 其他人可以用Alice的公钥验证签名的有效性在没有私钥的情况下无法伪造签名ECDSA基于椭圆曲线的数字签名2.钱包地址每个地址对应了一对公私钥私钥=>公钥=>钱包地址3.靠哈希算法出块pow暴力枚举计算哈希小于某个值或者哈希值前面有多少个0没有除了暴力枚举计算满足要求的哈希 通过算力的分散性保证去中心化出块4.靠哈希算法维护最长链,防止攻击每一个块的头部都包含上一个区块的哈希值想要修改之前区块的某个内容 需要从那个区块开始后面的所有区块都需要修改枚举哈希很难 很难对抗全网算力算出新链5.默克尔树一种机遇哈希的树状结构依靠哈希快速确认某个值是否在一个集合中的数据结构常用于区块存储交易 发行白名单确认等场景某个节点的哈希值生成父节点的哈希值直至根节点的哈希值 只要改了一个节点的数据就会导致根节点的哈希值变化四、layer 21.rollupRollup 是什么?Rollup 是一种区块链扩容技术,旨在解决以太坊等区块链网络的高 Gas 费和低吞吐量问题。它的核心思想是:将大量交易“打包”到链下计算,仅将少量关键数据提交到主链(Layer 1)。利用主链的安全性,同时大幅提升交易处理速度并降低成本。1.1 Rollup 的核心原理(1)交易执行移到链下传统区块链(如以太坊主网):每个节点需要执行并验证所有交易,导致速度慢、成本高。Rollup:交易在链下(Layer 2)批量执行。仅将交易数据的压缩摘要(Merkle 根)和状态变化证明提交到主链。(2)依赖主链保障安全性Rollup 的最终结算和争议处理仍依赖 Layer 1(如以太坊)。即使链下运营商作恶,用户仍可通过主链挑战并恢复资金。1.2. Rollup 的两种主要类型Rollup 分为两类,区别在于如何证明交易的有效性:类型Optimistic RollupZK-Rollup(零知识证明 Rollup)验证方式默认信任,欺诈时挑战(Fraud Proof)每笔交易用零知识证明(ZK-SNARKs)验证最终性7 天挑战期(延迟高)即时确认(延迟低)计算开销低(无需复杂证明)高(生成 ZK 证明需要大量计算)隐私性透明(交易数据公开)可选隐私(ZK 可隐藏细节)代表项目Arbitrum, OptimismzkSync, StarkNet, Scroll(1)Optimistic Rollup(乐观 Rollup)核心假设:假设所有交易都是诚实的,除非有人提出挑战。工作流程:运营商批量处理交易,提交状态根到主链。如果有欺诈,挑战者可在 7 天内提交欺诈证明(Fraud Proof)。主链验证后回滚错误交易。优点:兼容 EVM(适合通用智能合约),Gas 费低。缺点:提款需要等待挑战期(约 7 天)。(2)ZK-Rollup(零知识证明 Rollup)核心机制:每批交易生成一个零知识证明(ZK-SNARK/STARK),证明交易有效。工作流程:交易在链下执行并生成 ZK 证明。证明和状态变化提交到主链。主链验证证明后立即确认交易。优点:即时最终性(无需等待挑战期)。更高吞吐量(证明可压缩大量交易)。缺点:生成 ZK 证明计算复杂,对通用智能合约支持较晚(如 zkEVM)。1.3. Rollup 的关键技术(1)数据压缩Rollup 将交易数据压缩后存储在链上(如仅保存交易哈希、签名聚合)。例如:普通转账:原始数据 100B → 压缩后 10B。智能合约调用:通过状态差异记录而非完整输入。(2)状态根(State Root)表示 Rollup 链的当前状态(账户余额、合约存储等)。主链只需存储最新状态根,而非完整历史。(3)欺诈证明 vs 有效性证明 Optimistic Rollup(欺诈证明)ZK-Rollup(有效性证明)证明类型仅在争议时生成每批交易必须生成安全性依赖诚实多数假设数学证明(密码学保证)延迟高(需挑战期)低(即时验证)1.4. Rollup 的典型应用(1)DeFi(去中心化金融)Uniswap 在 Arbitrum(Optimistic Rollup)上部署,降低交易费。dYdX 使用 StarkEx(ZK-Rollup)实现高性能衍生品交易。(2)NFT 和游戏Immutable X(基于 StarkWare)提供零 Gas 费的 NFT 交易。Sorare(足球 NFT 游戏)使用 ZK-Rollup 处理海量交易。(3)支付网络Loopring(ZK-Rollup)实现低成本、高速的代币转账。1.5. Rollup vs 其他扩容方案方案吞吐量安全性兼容性代表项目Rollup高依赖主链高(EVM 兼容)Arbitrum, zkSyncPlasma中依赖退出机制低(仅支付)OMG Network侧链(Sidechain)高独立安全性中(需跨链桥)Polygon PoS状态通道极高依赖参与者在线低(特定场景)Raiden Network1.6. 未来发展方向ZK-EVM:让 ZK-Rollup 完全兼容以太坊智能合约(如 zkSync 2.0、Scroll)。混合 Rollup:结合 Optimistic 和 ZK 的优势(如 Optimism 未来可能集成 ZK)。去中心化排序器:防止 Rollup 运营商垄断交易排序(当前多为中心化控制)。总结Rollup 通过链下执行 + 链上验证,在保持安全性的同时大幅提升性能,是以太坊扩容(如 Danksharding)的核心组件。选择 Optimistic 还是 ZK 取决于需求:追求低成本和 EVM 兼容性 → Optimistic Rollup(Arbitrum/Optimism)。需要即时最终性和高吞吐 → ZK-Rollup(zkSync/StarkNet)。2.零知识证明2.1. 零知识证明是指一方(证明者)向另外一方(验证者)证明一个陈述是正确的 而无需透露除该陈述是正确外的其他信息证明者:负责计算交易并且把这些交易聚合成零知识证明验证者:验证证明者提交的零知识证明的有效性验证者发送一个随机数,证明者使用私钥生成签名发送给验证者,验证者根据公钥验证签名的合法性2.2 zk-SNARK非交互性:证明者向验证者一次性发送一个消息,两者无需进行交互。简洁性:验证速度快,存储空间小。核心逻辑:预先的“魔法仪式”(可信设置):一群人共同生成一组公共参数(类似“魔法黑板”),包含加密的秘密数字。完成后,原始秘密会被销毁,确保无人作弊。证明者生成“密码纸条”:你用公共参数和你的秘密(如方程的解),计算出一个简短的证明(类似“纸条”)。这个证明利用了数学魔法(椭圆曲线、多项式),使得:验证者能快速检查。但无法从证明反推你的秘密。应用:zcash:一种隐私优先的加密货币,允许用户选择性地隐藏交易信息(发送方、接收方、金额),同时保证交易合法性。核心功能:屏蔽交易(Shielded Transactions):使用 zk-SNARK 证明交易有效,但不透露任何细节。透明交易(Transparent Transactions):类似比特币,公开所有信息(可选)。工作流程:隐私交易生成:发送方生成 zk-SNARK 证明(证明自己有权花费资金,且金额平衡)。交易中仅包含加密的金额和地址,以及 zk-SNARK 证明(约 200 字节)。矿工验证:矿工验证 zk-SNARK 证明的合法性,无需知道具体交易内容。如果证明有效,交易被打包到区块。举例:Alice 向 Bob 转 1 ZEC,但链上只看到“有人转了一笔钱”,不知道是谁、多少钱。只有 Alice 和 Bob 能通过“查看密钥”解密交易详情。filecoin:是一个去中心化存储市场,用户支付代币(FIL)存储文件,矿工提供存储空间并获得奖励。核心问题:如何证明矿工真的存储了用户的数据,且未作弊?解决方案:复制证明(PoRep):矿工必须证明自己存储了数据的唯一副本(防止重复存储同一份数据骗奖励)。时空证明(PoSt):矿工需持续证明自己仍在存储数据(防止中途删除数据)。zk-SNARK 的作用:压缩证明:矿工生成 zk-SNARK 证明,证明自己完成了存储任务。证明体积极小(几百字节),节省区块链空间。高效验证:网络节点只需验证 zk-SNARK,无需下载全部存储数据。举例:用户上传 1TB 电影到 Filecoin 网络。矿工 A 声称存储了该文件,并提交 zk-SNARK 证明。其他节点只需验证证明,无需下载 1TB 文件即可确认矿工诚实。2.3 zk Rollup项目zksync基于zk-SNARK实现安全依赖初始化信任设置,要求至少有一个参与者是诚信的evm兼容,可以把智能合约转换操作码来实现solidity兼容同时支持链上和链下存储starkware基于zk-stark技术实现相比zk-snark具备的优势透明性:zk- stark无需信任设置扩展性:zk-stark降低了计算复杂度,生成证明速度更快抗量子攻击:使用抗冲突的哈希函数提供抵御量子攻击
2025年05月14日
6 阅读
0 评论
2 点赞
1
...
6
7