面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 22:48

DNS 递归查询和迭代查询有什么区别?

递归查询:客户端问解析器,解析器负责从头到尾查完返回最终结果——客户端只发一次请求。迭代查询:解析器问根服务器拿到 TLD 地址,再问 TLD 拿到权威地址,再问权威拿到最终 IP——解析器自己一步步问。实际流程:客户端→解析器是递归,解析器→根/TLD/权威是迭代。追问为什么不全用递归?根/TLD 服务器不提供递归服务——它们服务全球数十亿客户端,每个递归请求都打到根服务器扛不住。根只告诉你下一步问谁(迭代),解析器负责串联。解析器怎么知道根服务器地址?内置的根提示文件(root hints),包含 13 组根服务器的 IP。BIND/Unbound 等软件自带此文件。递归查询的性能瓶颈在哪?冷启动(缓存全空)需要 4 次 RTT:解析器→根→TLD→权威→拿到 IP。热缓存时 1 次 RTT 甚至 0(本地缓存命中)。解析器缓存命中率是核心指标——正常 >90%。什么是转发解析器?不直接迭代查询,而是把请求转发给上游解析器(如企业 DNS 转发给 8.8.8.8)。减少出口 IP 方便防火墙管理。缺点:多一跳延迟,上游故障全部影响。DNS 预取(Prefetching)是什么?解析器在 TTL 即将过期前主动刷新缓存,避免用户请求命中过期缓存触发完整迭代查询。用户无感知,缓存保持热度。
服务端阅读 05月29日 22:48

DNS 根服务器是什么?全球有多少个?

根服务器是 DNS 层级最顶端,返回所有 TLD(.com/.org/.cn 等)的服务器地址。全球 13 组根服务器(A-M),13 是早期 UDP 512 字节包大小限制的结果。每组通过 Anycast 技术在全球部署数百个实例,实际根服务器节点超过 1000 个。追问只有 13 组不会单点故障吗?Anycast 解决——同一 IP 全球数百个节点,请求自动路由到最近的。某节点故障流量自动切到其他节点。加上解析器缓存,根服务器实际负载不高。根服务器被攻击会怎样?2015 年 DDoS 攻击导致部分根服务器离线数小时,用户基本无感知——解析器有缓存 TTL 未过期前不需要问根。长期(>48h)离线才会影响新域名首次解析。谁在管理根服务器?12 个组织(A-M 各一个),受 ICANN 的 IANA 监督。2016 年根区文件管理权从美国移交 ICANN。中国有根服务器吗?有镜像节点(Anycast 实例),但不是运营方。2019 年引入 F/I/K/L 四组根的镜像,提升国内解析速度和抗断网能力。根区文件仍由 ICANN 管理。如果 13 组全挂了怎么办?解析器缓存能撑数小时到数天。顶级域 TLD 缓存同理。短期全挂影响有限,长期需本地部署替代根区。
服务端阅读 05月29日 22:48

DNS 负载均衡怎么实现?有哪些方案?

DNS 负载均衡就是同一域名返回多个 IP,客户端随机选一个访问。三种方案:轮询(Round Robin)——权威服务器按顺序返回不同 IP,简单但无法感知服务器负载;加权 DNS——不同 IP 返回不同比例,权重控制流量分配;GeoDNS——根据客户端 IP 所在地区返回最近的服务器 IP,CDN 核心技术。局限:无法做健康检查、受 TTL 缓存影响切换慢、客户端可能缓存某个 IP 不轮询。追问DNS 负载均衡和 Nginx 负载均衡有什么区别?DNS 在域名解析层分配 IP,客户端直连服务器——无中间层延迟但无法精细控制。Nginx 在应用层代理——可做健康检查、会话保持、权重动态调整,但增加一跳延迟。生产环境常组合:DNS 做全局负载(多机房),Nginx 做单机房内负载。TTL 设多少合适?负载均衡场景一般 60-300 秒。故障切换时临时改 TTL 为 30 秒。注意运营商 DNS 可能强制延长最低 TTL。如何实现 DNS 健康检查?DNS 本身不做健康检查。需要外部系统:监控服务检测后端可用性,故障时调用 DNS API 删除该 IP 记录。Route53/Cloudflare DNS 都有内置健康检查+自动故障切换。GeoDNS 的原理?权威服务器根据请求来源 IP 判断地理位置(用 GeoIP 数据库),返回该地区最近的服务器 IP。实际用 Anycast + EDNS Client Subnet(ECS)传递客户端子网信息。DNS 负载均衡能做灰度发布吗?能但粗糙——给新版本分配 5% 的 IP 权重逐步提升。粒度太粗且受 TTL 缓存影响。精细灰度用 Istio/Nginx 按请求头/cookie 路由。
服务端阅读 05月29日 22:48

如何用 cURL 编写 API 自动化测试脚本?

cURL 测试脚本 = cURL 命令 + shell 判断 + 状态码/响应体验证。核心模式:curl -s -o /dev/null -w "%{http_code}" URL 拿状态码;curl -s URL | jq .field 提取响应体字段;for/while 循环批量测试。进阶:--fail 返回非零表示 HTTP 错误、--max-time 10 超时控制、--retry 3 重试。追问cURL 和 Postman 做测试哪个好?cURL 适合 CI/CD 管道和脚本化——轻量、无 GUI 依赖、易版本控制。Postman 适合手动探索和团队协作。生产环境两者结合:开发用 Postman 调试,CI 用 cURL 验证。怎么验证响应体内容?curl -s URL | jq -e .status==200,-e 让 jq 在结果为 false 时返回 exit code 1。多字段验证:jq -e ".status==200 and .data.id != null"。如何做性能基准测试?curl -s -w "timetotal: %{timetotal}s\n" -o /dev/null URL。批量测用循环+awk 算平均值。注意 cURL 不含 DNS 缓存预热,首次请求偏慢。怎么处理需要登录的接口?先 curl 登录拿 token:TOKEN=$(curl -s -X POST login-url -d user/pass | jq -r .token),后续带 -H "Authorization: Bearer $TOKEN"。cURL 脚本的缺点?无测试报告、无并行执行、错误定位不友好。超过 20 个接口建议上专用测试框架(pytest+requests/Jest+supertest),cURL 适合轻量级冒烟测试和 CI 快速验证。
服务端阅读 05月29日 22:48

DNS 缓存怎么工作?TTL 起什么作用?

DNS 查询每级都缓存:浏览器缓存(Chrome 约 1 分钟)→ 操作系统缓存(TTL 决定)→ 本地解析器缓存(运营商/企业 DNS)→ 权威服务器。TTL(Time To Live)是权威服务器设定的缓存有效期,单位秒。TTL=300 表示缓存 5 分钟内可直接使用,过期后必须重新查询。越短数据越新但查询越多,越长越快但变更延迟越大。追问浏览器 DNS 缓存怎么看?Chrome 地址栏输入 chrome://net-internals/#dns 查看缓存条目和过期时间。运营商会篡改 TTL 吗?会。部分运营商 DNS 强制最低 TTL(5 分钟甚至 1 小时),无视权威设定的短 TTL。对策:用权威 DNS 服务商的低 TTL + 多地探测验证。负 DNS 缓存是什么?缓存 NXDOMAIN(域名不存在)结果,TTL 由 SOA 的 minimum 字段决定。防止频繁查询不存在的域名。域名刚注册后负缓存未过期,部分地区仍解析失败——等 TTL 过期即可。如何强制刷新 DNS 缓存?macOS:sudo dscacheutil -flushcache。Linux:sudo systemd-resolve --flush-caches。Windows:ipconfig /flushdns。运营商缓存无法控制,只能等 TTL 过期。TTL 设为 0 会怎样?每次查询都穿透到权威服务器,不缓存。生产环境最低建议 60 秒。CDN 场景通常 300-3600 秒。
服务端阅读 05月29日 22:40

区块链有哪些安全威胁?51%攻击和双花攻击怎么防?

三大威胁:51%攻击——控制超半数算力/权益后可回滚交易;双花攻击——同一笔钱花两次;智能合约漏洞——重入、溢出、权限缺失。51%防御:PoS 中 slash 没收质押金让攻击成本远超收益。双花防御:等 6 个确认(BTC)或 finalize(ETH PoS 约 12 分钟)。合约防御:Checks-Effects-Interactions 模式、OpenZeppelin 库、审计。追问51%攻击在 PoS 链上更容易还是更难?更难。PoW 攻击后算力还在手上无损失。PoS 攻击被发现后质押金被 slash 没收,经济损失巨大且不可逆。交易所如何防双花?不信任 0 确认交易。大额等 6+ 确认,用区块链监控检测重组。女巫攻击和 51%攻击什么关系?女巫攻击是创建大量虚假身份影响共识,是 51%攻击的前提手段之一。PoW/PoS 都通过经济成本抑制。MEV 算安全威胁吗?算。前端运行抢跑、三明治攻击让用户成交价变差。Flashbots 等私有内存池将 MEV 收益转移给用户,但不能完全消除。形式化验证能替代审计吗?不能。只能验证你写出 spec 的部分,业务逻辑漏洞不违反 spec 就检测不到。验证+审计+测试网+Bug Bounty 四层防护。
服务端阅读 05月29日 22:40

区块链用了哪些密码学技术?哈希、签名和 Merkle Tree 各起什么作用?

三大核心:哈希函数(SHA-256/Keccak256)保证数据完整性,区块头哈希链接形成链;数字签名(ECDSA/EdDSA)私钥签名公钥验证,证明交易身份;Merkle Tree 只需 log(n) 个哈希即可证明某笔交易在区块中(SPV 验证)。三者协作:签名保证身份,哈希保证不可篡改,Merkle 保证高效验证。追问SHA-256 和 Keccak256 有什么区别?SHA-256 是 NIST 标准(比特币),Keccak256 是 SHA-3 竞赛获胜者(以太坊)。算法完全不同:SHA-2 是 Merkle-Damgard 构造,Keccak 是海绵构造。为什么不直接用公钥当地址?公钥 64 字节太长。以太坊地址=keccak256(pubKey) 后 20 字节,比特币地址=Base58Check(SHA-256+RIPEMD-160(pubKey))。缩短地址减少链上存储。Merkle 证明怎么验证?提供目标哈希+每层兄弟哈希,逐层向上组合计算根哈希,对比是否等于已知根。只需 log2(n) 个哈希值。零知识证明怎么用?zk-SNARK/zk-STARK:证明我知道 X 而不透露 X。应用:Zcash 隐私交易、zkRollup 批量证明、身份验证(证明年满 18 岁不暴露生日)。量子计算会破解这些密码吗?ECDSA 会被 Shor 算法破解。SHA-256 只被 Grover 算法削弱(等价密钥长度减半)。抗量子方案:签名用 lattice-based,哈希加倍长度。以太坊已有抗量子路线图。
服务端阅读 05月29日 22:40

Service Worker 调试有哪些常用方法和工具?

Chrome DevTools 是主战场:Application > Service Workers 面板可查看注册状态、更新、卸载;Sources 面板可打断点;Network 面板勾选 Bypass for network 绕过 SW。Console 里 self.addEventListener(fetch, e => console.log(e.request.url)) 打日志最直接。开发时勾选 Update on reload 避免手动等待激活。追问SW 更新后页面还是旧逻辑?SW 更新走 install→wait→activate。wait 阶段等旧 SW 退出才激活。开发时勾选 Update on reload;生产用 skipWaiting()+clients.claim() 立即接管,但可能导致不兼容。推荐:显示提示让用户主动刷新。如何模拟离线场景?DevTools Network 选 Offline,或 Application > Service Workers 勾选 Offline。也可用 navigator.onLine 检测。注意:离线需要 SW 有缓存策略否则白屏。SW 里 console.log 看不到?SW 运行在独立线程,日志在 Sources > Service Worker 专用控制台。或在 Application > Service Workers 点 inspect 打开专用 DevTools。缓存没命中怎么排查?打印 caches.match(request) 结果:命中返回 Response,未命中 undefined。常见原因:URL 查询参数不一致、request method 不匹配(默认只匹配 GET)、vary 头导致缓存分裂。生产环境如何监控 SW 异常?self.addEventListener(error/unhandledrejection, …) 捕获异常上报。关注注册失败率和缓存命中率。Workbox 的 workbox-google-analytics 可追踪离线 PV。
服务端阅读 05月29日 22:40

DeFi 是什么?核心协议和流动性挖矿怎么运作?

DeFi 是在区块链上用智能合约替代传统金融中介的开放金融体系。三大核心协议:DEX(Uniswap 用 AMM 自动做市取代订单簿)、借贷协议(Aave/Compound,超额抵押借款,利率由供需算法决定)、衍生品(dydx 链上永续合约)。流动性挖矿:用户向资金池提供流动性获得 LP 代币+治理代币奖励。追问AMM 和订单簿哪个好?AMM 无需撮合随时可交易,适合长尾资产。但大额滑点大且有无常损失。订单簿体验接近 CEX,但需要链下撮合引擎。什么是无常损失?LP 提供两种代币后价格比例变化,LP 持有的资产价值低于简单持有两种代币。差值即无常损失。2 倍价格变化约 5.7% 损失。超额抵押为什么不高效?借 100 美元需存 150 美元抵押品。闪电贷解决了这个问题——同一交易内借还无需抵押,但只能用于原子操作。DeFi 的系统性风险?合约漏洞、预言机操纵、治理攻击、可组合性传染。2022 年 Celsius/Luna 崩盘是典型传染案例。流动性挖矿还有利可图吗?蓝筹 APR 从 2020 年 100%+ 降到个位数。现在要看:挖矿收益+交易手续费+代币增值预期综合评估。纯通胀模式不可持续。
服务端阅读 05月29日 22:40

Zustand 有哪些常用中间件?怎么用?

Zustand 中间件就是高阶函数,用函数组合串联:create(devtools(persist(immer((set) => …)))) 从内到外依次包裹。常用 5 个:persist 持久化到 storage;immer 支持 mutable 写法更新嵌套对象;devtools 接入 Redux DevTools;subscribeWithSelector 精确订阅子属性变化;combine 合并多个 slice。追问中间件的执行顺序有讲究吗?有。从内到外:最内层的先执行。devtools(persist(immer(…))) 意味着 immer 先处理,再 persist,最后 devtools 记录。顺序反了会出错。immer 和普通 set 的性能差异?immer 用 Proxy 追踪变更,有额外开销但通常可忽略。大多数项目 immer 的开发体验收益远大于性能损失。自定义中间件怎么做日志?拦截 set 参数,前后打印状态即可。生产环境用 devtools 替代手动日志。persist 的 partialize 怎么用?partialize: (state) => ({ token: state.token, theme: state.theme }) 只持久化指定字段,避免把临时 UI 状态也存进 storage。subscribeWithSelector 解决什么问题?默认 useStore(s => s.items) 用 Object.is 比较,对象每次都是新引用所以总是重渲染。subscribeWithSelector 让你可以用 shallow 比较或自定义 equalityFn。
服务端阅读 05月29日 22:40

PoW、PoS 和 DPoS 共识机制有什么区别?优缺点各是什么?

PoW:算力竞争出块,安全但耗能(BTC 每笔约 1000 kWh)。PoS:持币量决定出块权,节能 99.9% 但有质押中心化风险。DPoS:投票选验证者(如 EOS 21 节点),出块快但去中心化程度低。选型:极致安全选 PoW,效率可扩展选 PoS,高 TPS 选 DPoS。Ethereum 已从 PoW 转为 PoS。追问PoS 不是更容易富者愈富吗?是的,这是核心争议。缓解:设质押上限、slashing 惩罚、流动性质押降低门槛。完全消除不现实,但比 PoW 的矿机垄断门槛低。DPoS 为什么被批评不够去中心化?21 个节点就能控制整条链,容易串谋。选民冷漠也加剧集中。支持者认为出块快(0.5s)、吞吐高,适合特定商业场景。Ethereum 为何选 PoS 不选 DPoS?Casper FFG 允许任意 32 ETH 质押者参与(超 50 万验证者),远比 21 节点分散。DPoS 的高 TPS 代价是牺牲去中心化,Ethereum 选 L2 Rollup 扩容。PoW 的 51%攻击成本怎么算?比特币约需 200 万台矿机,成本超 50 亿美元+每日电费数百万。攻击后 BTC 价格暴跌,矿机变废铁。小算力链就没有这个保障。还有其他共识机制吗?PoA(权威证明,联盟链)、PoH(历史证明,Solana)、BFT 类(Tendermint,2/3 投票确认,确定性快但不适合大规模验证者)。
服务端阅读 05月29日 22:40

Service Worker 如何实现跨域资源的缓存?

Service Worker 可以缓存跨域资源,但有 CORS 限制:fetch 跨域请求默认不发 credentials,响应必须包含 Access-Control-Allow-Origin 头,否则浏览器拦截无法缓存。opaque response(no-cors 模式下)可缓存但无法读取内容,且计入 7 倍缓存配额,最多存活 7 天(Chrome)。CDN 支持 CORS 时务必用 cors 模式。追问opaque response 有什么坑?status 始终为 0,无法读取 body 也不能判断缓存是否有效。占用 7 倍缓存配额,7 天后自动清理。CDN 支持 CORS 务必用 cors 模式。如何让第三方 CDN 走 CORS?CDN 侧配置 Access-Control-Allow-Origin。SW 侧 fetch 时 mode: cors(默认值)。CDN 不支持 CORS 只能退回 no-cors。缓存跨域字体和图片有什么区别?字体必须 CORS 才能被 @font-face 使用(浏览器安全策略)。图片/CSS/JS 不受此限制,但 SW 缓存时 opaque response 仍有 7 天限制。Stale-While-Revalidate 怎么用?caches.match 返回缓存,后台 fetch 更新缓存。先快后新,下次请求就是新数据。跨域 API 的缓存怎么处理?API 通常带 CORS 头可正常缓存。但带 Authorization 的请求不能缓存到共享 Cache,会泄露用户数据。私有数据只缓存内存或 SessionStorage。
服务端阅读 05月29日 22:40

Zustand 中如何用 TypeScript 确保类型安全?

定义 interface StoreState 声明所有状态和 action 类型,create()((set) => …) 传入泛型。关键:set((state) => ({ count: state.count + 1 })) 这种函数式更新需要泛型才能正确推断 state 类型。Action 也写在 interface 里,类型签名一目了然。追问set 的类型怎么写才不报错?set 接受 Partial | ((state: StoreState) => Partial)。用 immer middleware 时写法是 set((state) => { state.count += 1 }),不需要返回值。selector 类型怎么保证?useStore(s => s.count) 自动推断为 number。复杂 selector 用 shallow 比较避免重复渲染。不要写 useStore 丢失类型。persist middleware 的泛型怎么传?persist 单独传泛型,和 create 的泛型一致。漏传泛型会导致 set 内部类型丢失。多个 slice 怎么组织类型?按功能拆分文件,每个 slice 导出自己的 interface 和 create,主 store 用组合模式合并。和 Jotai 的类型体验比呢?Jotai 的 atom 天然类型安全,不需要额外泛型。Zustand 需要手动传泛型但更灵活。两者都能做到完全类型安全。
服务端阅读 05月29日 22:40

Zustand 和 Redux 有什么区别?选哪个?

核心区别:Zustand 不需要 Provider 包裹、不需要 reducer/action 模板代码、store 直接用 set 修改。Redux 需要 Provider + createSlice + useDispatch + useSelector 四件套。Zustand 1KB vs Redux+RTK 30KB。选型:新项目/中小团队选 Zustand,已有 Redux 代码库或需要强约束选 Redux。追问Zustand 没有 DevTools 吗?有。import { devtools } from zustand/middleware,用法和 Redux DevTools 一样,时间旅行、action 日志都支持。Redux 的 middleware 体系 Zustand 怎么替代?Zustand 用函数组合替代:create(devtools(persist(immer((set) => …)))) 一行串联。异步不需要 thunk/saga,直接在 action 里 async/await。大型项目 Redux 更稳吗?Redux 的约束在大型团队中减少出错概率。但 Zustand 配合 TypeScript + selector 约定也能达到类似效果。关键是团队规范。能混用吗?可以但不推荐——两套模式增加认知负担。迁移建议:新模块用 Zustand,旧模块保持 Redux,逐步替换。Immer middleware 和 RTK 的区别?都是用 Immer 实现不可变更新,语法几乎一致,底层都是 produce。区别只在包装层。
服务端阅读 05月29日 22:40

React Native 中如何使用 Zustand 管理状态?

和 Web 端完全一样——npm install zustand,create((set) => ({ … })) 创建 store,组件里 useStore(selector) 读取。Zustand 不依赖 DOM API,纯 JS 实现,React Native 直接能用。唯一需要注意的是持久化:Web 用 localStorage,RN 用 mmkv 或 AsyncStorage,配合 zustand/middleware 的 persist 中间件传入不同的 storage adapter。追问Zustand 和 Redux 在 RN 中哪个更合适?Zustand。RN 对包体积敏感,Zustand 1KB vs Redux+RTK 约 30KB。API 更简洁,不需要 Provider 包裹根组件。新项目优先 Zustand。如何做持久化存储?用 MMKV 而非 AsyncStorage——MMKV 同步读写,快 30 倍。persist 中间件传入 createJSONStorage(() => mmkvStorage) 即可。关键字段(token、用户偏好)必须持久化。多个页面共享状态会重复渲染吗?用 selector 精确订阅就不会。useStore(s => s.token) 只在 token 变化时重渲染。切忌 useStore() 全量订阅。后台被系统杀掉 store 会丢吗?没做持久化的会丢。RN 端内存回收后 JS 上下文重建,store 恢复初始值。关键字段必须 persist。导航中需要传递 store 吗?不需要。Zustand 是全局单例,任何组件直接 useStore 即可,不用像 Context 那样逐层传递。
服务端阅读 05月29日 22:35

Zustand 中如何处理异步操作?

Zustand 的 store 就是个普通对象,create 回调里直接写 async 函数即可,不需要 thunk 之类的中间件。写法:在 create((set, get) => ({ ... })) 里定义 async action,内部 await 拿到数据后调 set({ data, loading: false })。手动管理 loading/error 状态是最常见的方式。如果嫌重复,用 zustand/middleware 的 immer 简化嵌套更新,或封装一个 createAsyncAction 工具函数统一处理 loading/success/error 三态。追问Zustand 和 Redux Toolkit 处理异步有什么区别?RTK 需要 createAsyncThunk + extraReducers 处理三态,模板代码多。Zustand 直接在 action 里 await + set,没有中间件概念,代码量少一半以上。RTK 的优势是 DevTools 自动追踪异步状态,Zustand 需要手动 devtools middleware。并发请求怎么处理?两个独立请求各自 async action 并行调用即可。如果需要等全部完成:await Promise.all([fetchA(), fetchB()])。注意竞态问题——快速切换页面时旧请求后到会覆盖新数据,用请求 ID 或 AbortController 取消旧请求。Suspense 能配合 Zustand 用吗?可以,但需要包装成 throw promise 的模式:store 里存 promise 而非数据,组件读时如果 promise 未 resolve 就 throw 出去,Suspense 捕获。推荐用 use 包(React 19 内置)或 suspend-react 简化。不过大多数项目手动 loading 状态更直观,Suspense 方案适合设计系统级别统一处理。如何做请求缓存和去重?简单方案:store 里维护 Map<cacheKey, { data, timestamp }>,action 里先查缓存未过期就直接返回。复杂场景用 SWR 或 TanStack Query 管缓存,Zustand 只管 UI 状态,职责分离更清晰。服务端渲染(SSR)时异步怎么处理?create 时传入 hydrate 数据,客户端 useEffect 里发起请求覆盖。注意避免服务端和客户端数据不一致的 hydration mismatch——初始渲染用服务端数据,客户端请求完成后 set 更新即可。
服务端阅读 05月29日 22:35

Solidity 中 view、pure 和 payable 函数修饰符有什么区别?

view 可读不可写状态变量,pure 不可读也不可写,payable 允许接收 ETH。无修饰符的函数可读可写。view 和 pure 不消耗 gas(外部调用时),因为节点可以本地模拟执行而不上链。但 view/pure 在合约内部被交易调用时,调用者仍需付 gas。payable 的唯一作用是让函数能通过 msg.value 收到 ETH,非 payable 函数收到 ETH 会自动 revert。追问view 函数真的不花 gas 吗?外部调用(call / eth_call)不花 gas,因为是只读模拟。但如果一笔交易内部调用了 view 函数,那笔交易本身要付 gas——view 只是承诺不修改状态,不代表调用它的上下文免费。payable 和 non-payable 的 gas 差异?non-payable 函数开头会自动插入 require(msg.value == 0) 检查(约 200 gas)。payable 跳过这个检查,所以 gas 略低。如果函数明确需要收 ETH,加 payable 既是功能需求也省 gas。为什么编译器会警告"view 函数修改了状态"?因为你在 view 函数里调用了写操作(写 storage、发 ETH、触发事件等)。编译器按修饰符检查,不符合就报错。解决:要么去掉 view(确实需要写),要么确保只读。emit 事件在 view 函数中也不允许,因为事件本身是状态变更的日志。pure 函数里能用 block.timestamp 吗?不能。block.timestamp、block.number、msg.sender 都属于读取区块链状态,pure 里不允许。只允许用函数参数和内存变量做纯计算。如果你需要读链上状态但不写,用 view。接口中的 view/pure 声明有什么用?接口中声明 view/pure 是给编译器的契约——实现合约的对应函数也必须是 view/pure。如果实现合约把 view 改成 non-view,编译会报错。这保证了外部调用者可以安全地用 eth_call 调用而不用发交易。
服务端阅读 05月29日 22:35

Solidity 中 delegatecall 和 call 有什么区别?代理合约怎么实现?

call 在被调用合约的上下文执行,msg.sender 是调用者,存储读写被调用合约的 storage。delegatecall 在调用者的上下文执行,msg.sender 保持原始调用者不变,存储读写调用者的 storage——代码是别人的,存储是自己的。代理合约就是靠 delegatecall 实现的:代理合约存数据,逻辑合约存代码,fallback 函数 delegatecall 到逻辑合约,逻辑合约操作的是代理的 storage。追问透明代理和 UUPS 有什么区别?透明代理(Transparent Proxy):代理合约的 admin 调管理函数、user 调业务逻辑,在代理中用 if (msg.sender == admin) 分流,无函数选择器冲突风险但 gas 多约 2000。UUPS:升级逻辑写在逻辑合约里,代理更轻量,但逻辑合约忘了写 _authorizeUpgrade 就永远锁死——安全性依赖逻辑合约代码质量。存储碰撞怎么防?代理合约和逻辑合约的存储布局必须对齐——变量声明顺序一致,新版本只能追加变量不能删除或插入。用 OpenZeppelin 的 StorageSlot 或 EIP-1967 规定特定 slot 存代理管理数据(如 bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)),避免和业务变量撞 slot。代理合约升级时旧数据会丢吗?不会。数据存在代理合约的 storage 里,逻辑合约升级只是换了 delegatecall 的目标地址,代理的 storage 不变。但新逻辑合约的存储布局必须兼容旧布局——删除或重排变量会导致旧数据错位。为什么不直接用 call 替代 delegatecall?call 执行后状态变更在被调用合约上,调用者(代理)的 storage 不变——等于白调了。代理模式的核心就是"数据在代理、逻辑在实现",只有 delegatecall 能让实现合约操作代理的 storage。Beacon 代理是什么?多个代理合约共享同一个逻辑合约地址,升级时改一处全部生效。结构:Proxy → Beacon(存 implementation 地址)→ Logic Contract。好处是管理 100 个代理只需一次升级,gas 省。坏处是多一层间接调用。
服务端阅读 05月29日 22:35

Solidity 中如何安全地生成随机数?

Solidity 无法原生生成真随机数——block.timestamp、block.difficulty、blockhash 都可被矿工操纵,永远不要用于决定资金分配。安全方案分三类:Chainlink VRF(链上验证的链下随机数,生产首选)、commit-reveal(双方各提交哈希再揭示,适合两方博弈)、Randao/Drand(去中心化随机数网络,多节点协作生成)。Chainlink VRF 流程:请求时传 seed → Chainlink 链下生成随机数 + 证明 → 回调函数中验证证明后使用随机数,链上可验证不可篡改。追问blockhash 为什么不安全?blockhash(block.number - 1) 看似不可预测,但矿工可以选择性打包交易——如果随机结果对自己不利就不出块。而且 blockhash 只保留最近 256 个区块,超过就返回 0。Chainlink VRF 的 gas 消耗高吗?请求约 5 万 gas,回调验证约 15-20 万 gas(含证明验证)。总成本约 0.01-0.03 ETH。用 VRF v2 的订阅模式可以预充 Link,比直接支付更灵活。NFT mint、抽奖等高频场景需要考虑成本。commit-reveal 适合什么场景?两个参与者博弈(如石头剪刀布):先提交 keccak256(choice + secret) 的哈希(commit),双方都提交后再揭示原始值(reveal),验证哈希匹配。缺点是需要两轮交易,用户体验差,不适合多方或高频场景。如何防止前端运行随机数结果?即使随机数来源安全,攻击者可以在 mempool 中看到交易结果后决定是否抢跑。对策:用回调模式(结果在下一次交易中返回,而非同一交易)、加最小延迟、或使用 Flashbots 等私有内存池。游戏中随机数用什么方案?小额休闲游戏用 Chainlink VRF v2(成本可控、链上可验证)。大型链游用混合方案:VRF 生成种子 → 链上伪随机函数展开成序列 → 玩家行为(提交 nonce)参与混合,兼顾公平和性能。
服务端阅读 05月29日 22:35

Solidity 中 ECDSA 签名验证的原理是什么?如何实现?

ECDSA 签名验证就是用私钥签名、用公钥验证,链下签名链上验证。Solidity 用 ecrecover(hash, v, r, s) 从签名恢复出签名者地址,再对比是否为预期地址。标准流程:bytes32 hash = keccak256(abi.encodePacked(...)) → 用 EIP-712 结构化哈希 → 链下签名得 (r, s, v) → 链上 ecrecover 恢复地址。OpenZeppelin 的 ECDSA 库封装了边界检查和 malleability 防护。追问EIP-712 为什么比普通 keccak256 签名好?普通签名 keccak256(abi.encodePacked(...)) 用户看到的是一串十六进制,无法判断签了什么。EIP-712 定义了结构化的类型化数据,钱包(MetaMask)会显示人类可读的内容("你正在授权转移 100 USDC"),防止钓鱼签名。签名重放攻击怎么防?在签名数据中包含 nonce(递增计数器)和 address(this)(合约地址)。部署新合约后旧签名失效(地址变了),同一合约内每个 nonce 只能用一次。缺少 nonce 攻击者可以重复提交同一签名。ecrecover 返回 address(0) 意味着什么?签名无效时 ecrecover 不 revert,而是返回零地址。所以必须检查 recovered != address(0),否则攻击者构造一个恢复为零地址的签名就能绕过验证。OpenZeppelin 的 ECDSA.recover 已经内置了这个检查。签名延展性(Malleability)是什么?ECDSA 签名 (r, s) 中,s 可以替换为 n - s(n 是椭圆曲线阶数)得到另一个合法签名,签名不同但恢复的地址一样。EIP-2 要求 s 在曲线阶数的上半部分,ECDSA.toEthSignedMessageHash 和 OpenZeppelin 库都做了这个约束。多签钱包如何用 ECDSA 实现?链下收集足够多签名,链上逐一 ecrecover 验证,检查恢复出的地址都在授权列表中且不重复。Gnosis Safe 就是这个模式——不需要链上存储 nonce 状态,gas 更省,但需要链下协调签名顺序。