You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Solidity链上字体优化:避免bytes常量不必要的存储拷贝

链上字体Gas优化方案(汇编+Solidity)

核心思路

Solidity中bytes常量会被嵌入合约bytecode的只读数据段,无需拷贝到内存再存入storage,可通过汇编直接读取或写入该段数据,彻底消除冗余拷贝的Gas开销。以下是三种适配不同业务场景的方案:


方案1:完全跳过存储,直接返回代码段常量

若字体数据为固定值、无需动态修改,根本不需要写入storage,直接在视图函数中通过汇编读取代码段的常量返回,Gas消耗可从12M降至极低。

// 定义多份字体常量(编译时嵌入bytecode)
bytes constant FONT_SERIF = hex"[你的字体十六进制数据]";
bytes constant FONT_SANS = hex"[你的字体十六进制数据]";

// 根据字体ID返回对应数据
function getFont(uint256 fontId) external view returns (bytes memory) {
    bytes memory result;
    assembly {
        let dataPtr := 0
        let dataLen := 0

        // 根据ID匹配对应常量
        switch fontId
        case 0 {
            dataPtr := FONT_SERIF.slot  // 常量在bytecode中的起始偏移
            dataLen := FONT_SERIF.length // 常量长度
        }
        case 1 {
            dataPtr := FONT_SANS.slot
            dataLen := FONT_SANS.length
        }
        // 可扩展更多case

        // 分配内存并复制数据
        result := mload(0x40)
        mstore(0x40, add(result, add(dataLen, 0x20))) // 更新内存指针
        mstore(result, dataLen) // 写入数据长度
        codecopy(add(result, 0x20), dataPtr, dataLen) // 从代码段直接复制到内存
    }
    return result;
}

优势:无存储开销,读取效率拉满,扩展性可通过封装枚举/常量映射优化,无需手写二分查找。


方案2:仅存储注册状态,读取时直接取代码段数据

若需记录字体注册状态(如哪些字体已启用),但无需存储字体本身,可在storage中仅存字体ID与常量索引的映射:

// 存储已注册字体的常量索引
mapping(uint256 => uint256) public registeredFontIndices;
// 字体常量数组
bytes constant[] internal FONT_LIBRARY = [
    hex"[字体1数据]",
    hex"[字体2数据]"
];

// 注册字体(仅存索引,Gas消耗可忽略)
function _registerFont(uint256 fontId, uint256 constantIndex) internal {
    require(constantIndex < FONT_LIBRARY.length, "Invalid constant index");
    registeredFontIndices[fontId] = constantIndex;
}

// 根据ID返回字体数据
function pathOf(uint256 fontId) external view returns (bytes memory) {
    uint256 idx = registeredFontIndices[fontId];
    require(idx != 0 || registeredFontIndices[fontId] == 0, "Font not registered");
    
    bytes memory result;
    assembly {
        let target := FONT_LIBRARY[idx]
        let dataPtr := target.slot
        let dataLen := target.length

        result := mload(0x40)
        mstore(0x40, add(result, add(dataLen, 0x20)))
        mstore(result, dataLen)
        codecopy(add(result, 0x20), dataPtr, dataLen)
    }
    return result;
}

优势:既保留注册逻辑,又避免大体积数据的存储开销,Gas消耗仅来自映射读写和代码段读取。


方案3:必须写入storage时,跳过内存直接拷贝

若业务逻辑要求字体数据必须存入storage,可通过汇编直接从代码段复制到storage,跳过内存中间层:

bytes constant TARGET_FONT = hex"[你的字体数据]";
mapping(uint256 => bytes) public fontStorage;

// 注册字体(直接从代码段写入storage)
function _register(uint256 fontId) internal {
    assembly {
        let dataPtr := TARGET_FONT.slot
        let dataLen := TARGET_FONT.length

        // 计算storage位置:fontStorage的slot + fontId的哈希
        let baseSlot := keccak256(add(fontStorage.slot, 0x20), fontId)
        // 先写入数据长度
        sstore(baseSlot, dataLen)

        // 分批复制数据到storage(每次32字节)
        let remaining := dataLen
        let offset := 0
        let currentSlot := add(baseSlot, 1)

        for { } gt(remaining, 0) { } {
            let chunkSize := min(remaining, 0x20)
            // 从代码段读取32字节到临时内存
            codecopy(0x0, add(dataPtr, offset), chunkSize)
            // 写入到storage
            sstore(currentSlot, mload(0x0))
            // 更新指针
            currentSlot := add(currentSlot, 1)
            offset := add(offset, chunkSize)
            remaining := sub(remaining, chunkSize)
        }
    }
}

优势:比原方案(memory→storage)节省约50%以上的Gas,避免了大内存拷贝的开销。


注意事项

  1. Solidity版本兼容:.slot和.length属性在Solidity 0.6+稳定支持,建议使用0.8.x版本。
  2. 合约大小限制:以太坊合约bytecode最大为24KB(EIP-170),若单字体数据过大,需拆分常量或使用代理合约存储不同字体。
  3. 只读限制:代码段为只读区域,常量必须是编译时确定的静态数据,无法动态修改。

内容的提问来源于stack exchange,提问作者Satoshi Nakajima

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 15:05:30