合约升级标准 ERC2535 的设计解析和不足
合约升级标准 ERC2535 的设计解析和不足
Safful最近审计了钻石标准的一份实现代码,这一标准是一种新的可升级合约模式。撰写标准是一项值得赞许的事业,但钻石标准及其实现有许多引人担忧的地方。这份代码是过度工程的产物,附带了许多不必要的复杂性,所以现在我们不能推荐使用。
当然,钻石标准提议还处在草稿阶段,有成长和改进的空间。一套有用的可升级合约保证应该包含:
- 一个清晰的、简单的实现。标准应该易于阅读,以化简与第三方应用的集成流程。
- 一个升级流程的通盘检查清单。升级是有风险的,因此必须有透彻的解释。
- 对大多数常见升级错误(包括函数遮挡和函数碰撞)的链上缓解措施。许多错误虽然易于检测出来,但都能导致服务出错。见 “slither-check-upgradeability” 一文了解许多可以化解的陷阱。
- 相关风险的清单。合约可升级性不是简单的事,它可能遮掩了安全上应当考虑的问题,或者传递低估风险的暗示。EIP 是提升以太坊的提议,不是商业广告。
- 集成了常见测试平台的测试。测试应该凸显出如何部署系统、如何升级一个新的实现,以及升级在哪些条件下会失败。
不幸的是,钻石提案没有满足所有这些要求。这实在太糟糕了,因为我们想看到的是一个可以解决、至少是减轻可升级合约的主要安全陷阱的标准。从根本上来说,标准的撰写人必须明确假设开发者会犯错,并且以开发出能缓解错误的标准为目标。
不过,我们 还是能从钻石提案中学到很多。请继续往下读:
- 钻石标准如何工作
- 我们的复查揭示了什么
- 我们的建议
- 可升级标准的最佳实践
钻石标准范式
钻石标准是由 EIP 2535 定义的、还在开发中的工作。提案的草稿声称要给予 delegatecall 方法提出合约升级的一种新范式。(我们曾撰写过一份关于合约如何升级的概述,仅供参考。)EIP 2535 提议使用:
- 与实现合约适配的查找表(lookup table)
- 任意的存储指针(arbitrary storage pointer)
查找表
基于 delegatecall 的升级方法主要使用两个组件:一个代理合约和一个实现合约
图 1. 单一实现合约的基于 delegatecall 的升级方法
用户与代理合约交互,代理合约向实现合约发送 delegatecall 调用实现合约内的函数。执行的是实现合约内的代码,但整套合约的 storage 保存在代理合约内。
使用了查找表,代理合约就可以向多个实现合约发起 delegatecall 调用,可根据要调用的函数来选择合适的实现合约:
图 2. 多实现合约的基于 delegatecall 的升级方法
这种模式不是什么新东西。之前也有其他项目使用过这样的查找表来实现可升级性。ColonyNetwork 就是一个例子。
任意的存储指针
钻石提案还建议使用 Solidity 最近引入的一个功能:任意的存储指针(arbitrary storage pointer)。这个功能名副其实,就是允许你把一个 storage 的指针指向任意一个位置。
因为 storage 都存储在代理合约里,实现合约的 storage 布局必须与代理合约的 storage 布局保持一致。在升级的时候,很难跟踪这种布局(此处有一个例子)。
这个 EIP 提议,每个实现合约都要有一个相关联的结构体(structure)来保管实现合约的变量(variables),然后用一个指针指向存储该结构体的 storage 位置。这类似于 “unstructured storage” 模式,也是 Solidity 的一个新功能,支持使用一个结构体来替代一个单一的变量。
此处的假定是:来自两个不同实现的结构体不可能冲突,只要它们的基础指针(base pointer)不同。
bytes32 constant POSITION = keccak256("some_string");struct MyStruct {uint var1;uint var2;}function get_struct() internal pure returns(MyStruct storage ds) {bytes32 position = POSITION;assembly { ds_slot := position }}
图 3. storage 指针的例子
图 4. storage 指针的表示
插句话,什么是 “钻石(diamond)”?
EIP 2535 提出了一套 “钻石术语”,其中,“钻石” 指的是代理合约,“雕琢面(facet)” 指的是实现合约。等等。不太明白为什么要发明这套黑话,因为可升级性的标准术语都已经得到定义,而且众所周知了。我们这里做了一个列表来帮你翻译这套提案:
钻石标准术语 | 常见用名 |
---|---|
Diamond | Proxy(代理合约) |
Facet | Implementation(实现合约) |
Cut | Upgrade(升级) |
Loupe | List of delegated functions(delegated 函数的列表) |
Finished diamond | Non-upgradeable(不可升级的合约) |
Single cut diamond | Remove upgradeability functions(移除了可升级函数的合约) |
图 5. 钻石提案使用新的术语来指称已有的观念。
审计结果及建议
我们复查了钻石标准的实现,成果如下:
- 代码是过度工程的产物,包含了许多颠三倒四(misplaced)的优化
- 使用存储指针带有风险
- 代码有函数隐藏(function shadowing)
- 合约缺少存在性检查(existence check)
- 钻石术语带来了不必要的复杂性
过度工程的代码
虽然 EIP2535 所提议的模式是直接了当的,但其实际实现却难以阅读,也难以审核,因此提高了出问题的概率。
举个例子,在链上保存许多数据是很麻烦的。虽然这个提案只需要用到查找表,从函数的签名映射到实现合约的地址,但 EIP 定义了许多接口,需要把额外的数据存为 storage:
interface IDiamondLoupe {/// These functions are expected to be called frequently/// by tools.struct Facet {address facetAddress;bytes4[] functionSelectors;}/// @notice Gets all facet addresses and their four byte function selectors./// @return facets_ Facetfunction facets() external view returns (Facet[] memory facets_);/// @notice Gets all the function selectors supported by a specific facet./// @param _facet The facet address./// @return facetFunctionSelectors_function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);/// @notice Get all the facet addresses used by a diamond./// @return facetAddresses_function facetAddresses() external view returns (address[] memory facetAddresses_);/// @notice Gets the facet that supports the given selector./// @dev If facet is not found return address(0)./// @param _functionSelector The function selector./// @return facetAddress_ The facet address.function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
图 6. 钻石合约的接口
在这里,facetFunctionSelectors 会返回一个实现合约的所有函数选择器。这个信息其实只对链下的部件有用,而链下的部件完全可以从合约的事件中抽取出这些信息。其实根本没必要在链上放置这个功能,尤其,它还会极大地增加代码的复杂性。
此外,大部分的代码复杂性,都是因为去优化了无需优化的位置。举个例子,用于更新实现合约的函数应该是直接了当的。获得一个新的地址和一个新的签名后,代理合约应该在查找表中更新对应的入口。但是,你看看,用来实现这个功能的部分函数,长下面这个样子:
// adding or replacing functionsif (newFacet != 0) {// add and replace selectorsfor (uint selectorIndex; selectorIndex < numSelectors; selectorIndex++) {bytes4 selector;assembly {selector := mload(add(facetCut,position))}position += 4;bytes32 oldFacet = ds.facets[selector];// addif(oldFacet == 0) {// update the last slot at then end of the functionslot.updateLastSlot = true;ds.facets[selector] = newFacet | bytes32(selectorSlotLength) << 64 | bytes32(selectorSlotsLength);// clear selector position in slot and add selectorslot.selectorSlot = slot.selectorSlot & ~(CLEAR_SELECTOR_MASK >> selectorSlotLength * 32) | bytes32(selector) >> selectorSlotLength * 32;selectorSlotLength++;// if slot is full then write it to storageif(selectorSlotLength == 8) {ds.selectorSlots[selectorSlotsLength] = slot.selectorSlot;slot.selectorSlot = 0;selectorSlotLength = 0;selectorSlotsLength++;}}// replaceelse {require(bytes20(oldFacet) != bytes20(newFacet), "Function cut to same facet.");// replace old facet addressds.facets[selector] = oldFacet & CLEAR_ADDRESS_MASK | newFacet;}}}
图 7. 升级函数
许多的力气都花在优化这个函数的 gas 效率上。但是,升级操作是很少用到的,所以不管花多少 gas ,都不可能是 gas 的重度消耗者。
另一个多此一举的例子是用按位操作来替代结构体:
uint selectorSlotsLength = uint128(slot.originalSelectorSlotsLength);
uint selectorSlotLength = uint128(slot.originalSelectorSlotsLength >> 128);
// uint32 selectorSlotLength, uint32 selectorSlotsLength
// selectorSlotsLength is the number of 32-byte slots in selectorSlots.
// selectorSlotLength is the number of selectors in the last slot of
// selectorSlots.
uint selectorSlotsLength;
图 8. 使用按位操作来替代结构体
*2020 年 11 月 5 日更新:*我们审计之后,参考实现已经改变,但其底层的复杂性仍然保留着。现在有三个参考实现,让用户更加摸不着头脑,也让对该提议的进一步审核更加困难。
我们的建议:
- 始终追求简洁,并尽可能把代码放在链下。
- 撰写一套新标准时,保证代码可读且易于理解
- 在实现优化之前,先分析清楚需求
指针风险
虽然有人主张只要基础指针不同,就不会发生冲突,但是,一个恶意的合约可以用来自另一个实现合约的一个参数造成冲突。所以,冲突是有可能的,原因在于 Solidity 存储变量和影响映射和数组的方式。举个例子:
contract TestCollision{// The contract represents two implementations, A and B// A has a nested structure// A and B have different bases storage pointer// Yet writing in B, will lead to write in A variable// This is because the base storage pointer of B// collides with A.ds.my_items[0].elemsbytes32 constant public A_STORAGE = keccak256("A");struct InnerStructure{uint[] elems;}struct St_A {InnerStructure[] my_items;}function pointer_to_A() internal pure returns(St_A storage s) {bytes32 position = A_STORAGE;assembly { s_slot := position }}bytes32 constant public B_STORAGE = keccak256(hex"78c8663007d5434a0acd246a3c741b54aecf2fefff4284f2d3604b72f2649114");struct St_B {uint val;}function pointer_to_B() internal pure returns(St_B storage s) {bytes32 position = B_STORAGE;assembly { s_slot := position }}constructor() public{St_A storage ds = pointer_to_A();ds.my_items.push();ds.my_items[0].elems.push(100);}function get_balance() view public returns(uint){St_A storage ds = pointer_to_A();return ds.my_items[0].elems[0];}function exploit(uint new_val) public{St_B storage ds = pointer_to_B();ds.val = new_val;}}
图 9. 存储指针冲突
在爆破中,写入 B_STORAGE 基础指针的内容实际上会写入 my_items[0].elems[0],而这个位置又是从 A_STORAGE 基础指针中读取得到的。一个恶意的合约所有者可以推送一个看起来无害,但实际上包含后门的升级。
这份 EIP 没有为防止此类恶意冲突提供指导。此外,如果一个指针先被删除然后又被重用,这个重用会导致数据泄漏。
我们的建议:
- 操纵底层的存储是风险很大的,所以在设计依赖于底层存储的系统时必须格外小心。
- 使用带有结构体的非结构化存储来实现合约升级,是一个很有趣的思路,但需要详细的文档和指导来说明要在一个基础指针中检查什么东西。
函数遮挡
可升级合约组合的代理合约中通常会有一些函数,遮挡掉应该被 delegate 调用的函数。直接调用(call)这些函数无法被 delegate 传递至实现合约,因为只会使它们在代理合约内执行。此外,相关的代码也是不可升级的。
contract Proxy {constructor(...) public{// add my_targeted_function()// as a delegated function}function my_targeted_function() public{}fallback () external payable{// delegate to implementations}
}
图 10. 函数遮挡问题的简化例子
虽然这个问题是众所周知的,而且代码也经过 EIP 作者的审核,但我们还是在合约中发现了两个这样的函数遮挡的实例。
我们的建议:
- 在开发可升级的合约时,slither-check-upgradeability 来捕捉函数遮挡。
- 这一问题也凸显了重要的一点:开发者也会犯错。任何新的标准都应该包含对常见错误的缓解措施,只要你想干得比定制化的解决方案更好。
如果您对自己的升级策略有任何疑问,请联系我们。我们将竭诚为您服务。
没有合约的存在性检查
合约代码的另一个常见失误是缺乏存在性检查。如果代理合约 delegate 到了一个不正确的地址,或者一个已经被毁弃的实现合约,即使没有执行任何代码,这次调用也会返回成功(见 Solidity 文档)。结果是,调用者不会注意到这个问题,但这种行为可能会破坏掉第三方合约集成。
fallback() external payable {DiamondStorage storage ds;bytes32 position = DiamondStorageContract.DIAMOND_STORAGE_POSITION;assembly { ds_slot := position }address facet = address(bytes20(ds.facets[msg.sig]));require(facet != address(0), "Function does not exist.");assembly {calldatacopy(0, 0, calldatasize())let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)let size := returndatasize()returndatacopy(0, 0, size)switch resultcase 0 {revert(0, size)}default {return (0, size)}}}
图 11. 不设合约存在性检查的 fallback 函数
我们的建议:
- 不管调用什么合约,都要检查合约的存在性。
- 如果担心 gas 的消耗,那就仅在调用不返回数据时执行这种检查,因为反过来(如果会返回一些数据)就表明确实执行了某些代码。
不必要的钻石术语
如前所述,钻石提案的阐述高度依赖于这些新发明的术语。这很容易出错,让审核变得困难重重,而且对开发者也没有任何好处。
- “钻石(diamond)” 是指使用其 “雕琢面(facet)” 中的函数来执行函数调用的合约。一个钻石可能有一个或多个雕琢面。
- “雕琢面” 一词来自钻石行业,指的是钻石的一个面,或者说平坦的表面。一个钻石有很多个雕琢面。在本标准中,雕琢面指的是带有一个或多个函数、执行一个钻石的功能的合约。
- “透镜(loupe)” 指的是用来观察钻石的放大镜。在本标准中,透镜指的是一个提供函数来观察钻石及其雕琢面的合约。
图 12. 该 EIP 定义的标准术语都是软件工程不相关的东西。
我们的建议:
- 使用通用、普及度高的词语,如果没实际用途,就不要去发明黑话。
钻石提案是一条死胡同吗?
如上所述,我们依然相信,社区能从一种标准化的可升级性方案中受益。但当前的钻石提案并不满足我们期待的安全性要求,相比定制化的实现也并没有带来足够多的好处。
不过,这个提案还只是一个草稿,可以变得更简洁、更优秀。即使并没有,它所使用的一些技术,比如查找表和任意存储指针,也值得继续研究。
如果您对自己的升级策略有任何疑问,请联系我们。我们将竭诚为您服务。
所以,可升级性是否普遍可行?
这几年来,我们已经审核过许多可升级的合约,也发表了很多分分析。可升级性是困难的、容易出错的,也会带来风险,所以我们总的来说仍然不推荐大家把它当成一种解决方案。但话说回来,如果你决心给合约增加可升级性,你应该:
- 考虑不需要 delegatecall 的升级模式(见 Gemini implementation)
- 透彻地了解现有的解决方案及其局限性:
- Contract upgrade anti-patterns
- How contract migration works
- Upgradeability with OpenZeppelin
相关文章:
合约升级标准 ERC2535 的设计解析和不足
合约升级标准 ERC2535 的设计解析和不足 Safful最近审计了钻石标准的一份实现代码,这一标准是一种新的可升级合约模式。撰写标准是一项值得赞许的事业,但钻石标准及其实现有许多引人担忧的地方。这份代码是过度工程的产物,附带了许多不必要的…...
【Vue】ElementUI实现登录注册
一、搭建项目 二、后台交互 三、CORS跨域 好啦今天到这了,希望能帮到你!!!...
linux 安装 wordpress
文章目录 linux 安装 wordpress1. wordpress 简介2. wordpress功能和特点3. 部署要求4. 环境搭建4.1 部署 nginx4.1.1 新增配置文件 4.2 部署 PHP74.2.1 查看当前版本4.2.2 YUM 安装 PHP74.2.3 查看 PHP 版本4.2.4 启动PHP-FPM4.2.5 修改配置文件4.2.6 重启服务 4.3 部署 mysql…...
LeetCode902最大为 N 的数字组合(相关话题:数位DP问题,递归遍历和减枝)
目录 题目描述 方法一递归遍历和减枝 方法二动态规划 数位DP问题拓展 通用方法论引入 <...
USB总线-Linux内核USB3.0主机控制器驱动框架分析(十二)
1.概述 如下图所示,Linux内核中USB主机体系结构由五部分组成,分别为Application Software、USB Class Driver、USB Core(USB Driver)、USB Host Controller Driver、USB Host Controller。应用程序处于用户空间,通过系统调用访问Class Drive…...
SQL模板-用户留存率计算
在这段实习中,我遇到了用户留存率计算的需求,这里做个总结。 首先来讲下,什么是用户留存? 在互联网行业中,用户在某段时间内开始使用应用,经过一段时间后,仍然继续使用该应用的用户。用户留存一…...
LeakCanary 源码详解(3)
上一篇:LeakCanary源码详解(2) 如果你是直接刷到这篇的,建议还是从1开始看,然后2,然后是这篇3,如果你只关注这篇的重点hprof 文件定位泄漏位置的感兴趣,可以试试直接读这篇ÿ…...
springboot使用SSE
1、pom文件 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency> 2、前端代码 <!DOCTYPE html> <html lang"en"> <head><meta ch…...
搞定ESD(一):静电放电测试标准解析
文章目录 一、基本术语与定义1.1 基本定义1.2 重要基本术语 二、静电放电发生器介绍2.1 静电放电发生器的特性:通用规范【GB/T17626.2-2018 标准】2.2 ESD 放电发生器电极规格要求:通用规范【GB/T17626.2-2018 标准】2.3 放电回路电缆的要求:…...
问界M7的诸多优点(自动驾驶走进我们的生活二)
博主一直在问界工厂工作,从未对自己工厂的车如此关注过;但问界系列上市后,经常在茶余饭后看B站视频,发现问界车越来越多不可比拟的优点如下: 一、绿牌 绿牌特权在重庆可以随时过桥,不受限号限制。 二、增…...
[运维|数据库] msql中的 FIND_IN_SET如何转化为pg数据中的ARRAY_POSITION的函数
在 MySQL 中,FIND_IN_SET 函数用于查找一个值是否存在于逗号分隔的字符串列表中。在 PostgreSQL 中,可以使用 string_to_array 函数将逗号分隔的字符串转换为数组,然后使用 ARRAY_POSITION 函数来查找值是否在数组中。 以下是如何将MySQL中的…...
LeetCode 面试题 05.03. 翻转数位
文章目录 一、题目二、Java 题解 一、题目 给定一个32位整数 num,你可以将一个数位从0变为1。请编写一个程序,找出你能够获得的最长的一串1的长度。 示例 1: 输入: num 1775(110111011112) 输出: 8 示例 2: 输入: num 7(01112)…...
Fiddler抓包工具配置+Jmeter基本使用
一、Fiddler抓包工具的配置和使用 在编写网关自动化脚本之前,得先学会如何抓包,这里以Fiddler为例。会抓包的同学可以跳过这一步,当然看看也是没坏处的…… 局域网络配置 将要进行抓包的手机与电脑连入同一局域网,电脑才能够抓到…...
IOTE 2023国际物联网展直击:芯与物发布全新定位芯片,助力多领域智能化发展
IOTE 2023国际物联网展,作为全球物联网领域的盛会,于9月20日在中国深圳拉开帷幕。北斗星通集团应邀参展,旗下专业从事物联网、消费类GNSS芯片研发设计的芯与物公司也随其亮相本届盛会。 展会上,芯与物展示了一系列创新的GNSS定位…...
【软件设计师-从小白到大牛】上午题基础篇:第二章 操作系统
文章目录 前言章节提要一、进程管理1、进程的状态2、前趋图3、进程的同步与互斥4、PV操作6、PV操作与前趋图7、死锁问题进程资源图(补充)真题链接 二、存储管理1、分区存储组织2、页式存储组织3、段式存储组织4、段页式存储组织5、快表6、页面置换算法单…...
【20230921】关于sing-box命令行程序开机自启动运行(Windows、Linux)
1 背景 sing-box是一个命令行程序,官网给出的教程是复制链接到Git Bash(windows)或终端运行(Linux)。每次开机都进行复制运行是一件繁琐的事情。 复制的内容其实就是下次并运行shell脚本,其实系统只需要运…...
LeetCode 75-02:字符串的最大公因子
前置知识:使用欧几里得算法求出最大公约数 func gcdOfStrings(str1 string, str2 string) string {if str1str2 ! str2str1 {return ""}return str1[:gcd(len(str1), len(str2))] }func gcd(a, b int)int{if b 0{return a}return gcd(b, a%b) }...
k8s1.19使用ceph14
一、静态 pv (rbd)方式 1、所有k8s节点安装依赖组件 注意:安装ceph-common软件包推荐使用软件包源与Ceph集群源相同,软件版本一致。 cat > /etc/yum.repos.d/ceph.repo << EOF [ceph] name=ceph baseurl=http://mirrors.aliyun.com/ceph/rpm-nautilus/el7/x86_…...
Leetcode 50. Pow(x, n)
文章目录 题目代码(9.19 首刷看解析) 题目 Leetcode 50. Pow(x, n) 代码(9.19 首刷看解析) 快速幂 class Solution { public:double myPow(double x, int n) {if(n 0)return 1;if(n 1)return x;if(n INT_MIN) { // 避免-n整…...
hive分区表的元数据信息numRows显示为0
创建分区表 CREATE TABLE `dept_partition`(`deptno` int, `dname` string, `loc` string) PARTITIONED BY (...
Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的图像剪切(ROI)功能(C++)
Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的图像剪切(ROI)功能(C) Baumer工业相机Baumer工业相机的图像剪切(ROI)功能的技术背景CameraExplorer如何使用图像剪切(ROI)功…...
【云原生】聊聊为什么需要docker以及其基础架构
为什么需要docker 在没有docker之前,我们开发、测试、生产其实是根据不同的服务器进行配置的,很可能因为软件配置不同而导致的生产事故,那么如果能较好的解决软件和配置等封装成一个可运行的软件,无需关注配置,那么是…...
“高级前端开发技术探索路由的使用及Node安装使用“
目录 引言1. Vue路由的使用2. VueNode.js的安装使用总结 引言 在当今互联网时代,前端开发技术日新月异,不断涌现出各种新的框架和工具。作为一名前端开发者,我们需要不断学习和探索新的技术,以提升自己的开发能力。本文将深入探讨…...
LeetCode 494.目标和 (动态规划 + 性能优化)二维数组 压缩成 一维数组
494. 目标和 - 力扣(LeetCode) 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 或 - ,然后串联起所有整数,可以构造一个 表达式 : 例如,nums [2, 1] ,可以在 2…...
[36c3 2019]includer
[36c3 2019]includer 题目描述:Just sitting here and waiting for PHP 8.0 (lolphp). 首先来了解一下临时文件包含之PHP - compress.zlib:// 在 php-src 里可以找到和 compress.zlib:// 有关的代码 | code 注意到 STREAM_WILL_CAST,涉及到 cast 经常…...
Python150题day10
④continue练习 从列表 Ist [1,3,5,2,7,9,10] 中输出所有的奇数,代码如下 lst [1, 3, 5, 2, 7, 9, 10] for item in lst: if item % 2 0: continue print(item) 在上述代码中,当遇到偶数时,continue 语句会跳过当前迭代&…...
Autosar工具-Davinci Developer
文章目录 前言一、Davinci Developer简介二、导航栏File(主要是用于保存、打开工程等操作)HomeProject(主要用于导入、导出arxml文件)Graphic(主要在SWC设计时使用,包含对图形界面下的设计工具)Window(主要就是对我们的Dev界面外形修改用的,使得界面更加方便我们使用(比如隐…...
js中的数据结构:栈,队列,链表,字典哈希表,树
栈:先进后出 队列:先进先出 链表: 单链表: 双链表: 环形链表:最后一个数据的next指针不是指向null,指向的是任意之间的一个数据,形成一个环 数组和链表的区别: 字典和哈…...
Verdi实现信号的平移
在Verilog/System verilog中,# xxx可以实现延迟指定时间的功能,而在使用verdi查看信号波形并进行分析时,同样也可以实现类似的功能。 (注:这种信号平移是有其应用场景的,例如,在某些仿真模型中,…...
Leetcode算法入门与数组丨6. 数组双指针、滑动窗口
文章目录 1 双指针基础知识1.1 双指针简介1.2 左右指针(对撞指针)1.3 快慢指针1.4 分离双指针 2 滑动窗口基础知识2.1 滑动窗口算法介绍2.2 滑动窗口适用范围2.3 固定长度滑动窗口2.4 不固定长度滑动窗口 1 双指针基础知识 1.1 双指针简介 双指针&…...
泉州网站公司建站/360建网站
问题环境CocosCreator工程,输出Android Studio工程,用Android Studio打开,根据提示升级相关的Gradle:然后打包,安装到安卓手机上,运行测试,咦,怎么闪退了?如果查看logcat日志&#x…...
wordpress浏览次数插件/软文营销文案
移动手机APP测试从零开始(高级篇) http://edu.csdn.net/course/detail/838 视频教程...
购物网站支付功能怎么做/在线网页制作
今天和大家分享一个能在手机上演示的高保真移动端元件库。 43页完整版案例(含页面跳转交互)预览地址:https://axhub.im/ax9/bace41c00c111653/#g1 135页元件库(只含单页交互)预览地址:https://axhub.im/a…...
如何建设网络营销网站/有没有免费的crm系统软件
linux2.6内核网桥分析2008-03-19 16:15Linux网桥分析一、网桥原理网桥(Bridge)也称桥接器,是连接两个局域网的存储转发设备,用它可以完成具有相同或相似体系结构网络系统的连接。一般情况下,被连接的网络系统都具有相同的逻辑链路控制规程(LL…...
查询网站建设/seo人员是什么意思
从股票市场崩溃到1932年(大萧条时期最惨的一年),经济一直在螺旋式下降,而这个下降趋 势又因为采取了某些措施的缘故而更加速了。按老规矩说,这些措施本该使经济复兴的,可 是事实恰好相反。为了保障投资者的利益,物价是…...
如何建设网站安全管理制度/主流搜索引擎有哪些
中国麻将机行业风云变幻,群英汇聚,历经十余载,有的早已黯然退出了历史舞台,有的也奄奄一息,有的却依然风光无限。国人爱休闲,就离不开麻将,数人相聚,而行方城之戏。日出三竿…...