在测试中用solidity批量生成的一些常见的内容


前言

用solidity批量生成一些内容,方便在测试中使用大量的地址和值


一、Solidity批量生成地址

1 . 代码如下

在Solidity中,你可以使用makeAddr函数来生成测试地址。这个函数通常用于开发和测试环境,以便于生成预定义的地址。下面是一个批量生成10个地址的示例代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AddressGenerator {
    // 用于生成预定义地址的函数
    function makeAddr(string memory name) internal pure returns (address) {
        bytes memory b = bytes(name);
        bytes32 hash = keccak256(abi.encodePacked(b));
        return address(uint160(uint256(hash)));
    }

    // 生成10个地址并存储在合约状态变量中
    address[] public generatedAddresses;

    constructor() {
        for (uint256 i = 0; i < 10; i++) {
            string memory playerName = string(abi.encodePacked("player", toString(i)));
            address playerAddr = makeAddr(playerName);
            generatedAddresses.push(playerAddr);
        }
    }

    // 辅助函数,将数字转换为字符串
    function toString(uint256 value) internal pure returns (string memory) {
        // 处理0的情况
        if (value == 0) {
            return "0";
        }
        // 数字转换为字符串的逻辑
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
    
    // 用于获取生成的地址列表
    function getGeneratedAddresses() external view returns (address[] memory) {
        return generatedAddresses;
    }
}

2.makeAddr 代码解释

  1. makeAddr函数
    • 这个内部函数接受一个字符串参数,并使用Keccak-256哈希函数生成一个地址。这个地址是通过对哈希值进行类型转换得到的。在foundry的forge-std/src/StdCheats.sol,中有类似的源代码:
function makeAddrAndKey(string memory name) internal virtual returns (address addr, uint256 privateKey) {
       privateKey = uint256(keccak256(abi.encodePacked(name)));
       addr = vm.addr(privateKey);
       vm.label(addr, name);
   }

   // creates a labeled address
   function makeAddr(string memory name) internal virtual returns (address addr) {
       (addr,) = makeAddrAndKey(name);
   }

这段代码展示了两个Solidity函数,它们用于在智能合约测试环境中生成地址和关联的私钥。这些函数通常用于创建测试账户,并且它们依赖于用于智能合约测试的虚拟机(如Foundry的Forge测试框架)提供的特定功能。

函数解释

  1. makeAddrAndKey函数

    • function makeAddrAndKey(string memory name) internal virtual returns (address addr, uint256 privateKey)
    • 这是一个内部虚拟函数,它接受一个字符串参数name,用于生成一个地址和对应的私钥。
    • privateKey = uint256(keccak256(abi.encodePacked(name)));
      • 使用Keccak-256哈希函数和abi.encodePacked对字符串name进行编码和哈希,然后将哈希值转换为uint256类型,作为私钥。
    • addr = vm.addr(privateKey);
      • 使用虚拟机的addr函数,根据私钥生成对应的地址。这在实际的以太坊环境中不是可能的,因为地址是通过公钥和ECDSA算法计算得出的,而不是直接从私钥生成。但在测试环境中,这种方法可以用来模拟地址生成。
    • vm.label(addr, name);
      • 使用虚拟机的label函数给生成的地址设置一个标签,方便在测试日志中识别。
  2. makeAddr函数

    • function makeAddr(string memory name) internal virtual returns (address addr)
    • 这是一个内部虚拟函数,它调用makeAddrAndKey函数,但只返回地址部分。
    • (addr,) = makeAddrAndKey(name);
    • 这行代码通过解构赋值,忽略makeAddrAndKey返回的私钥,只获取地址。

示例

假设你正在编写一个测试脚本,需要生成几个测试账户:

// 在测试脚本中
function testMyContract() public {
    address player1 = makeAddr("player1");
    address player2 = makeAddr("player2");
    // 进行一些测试操作...
}

在这个示例中,makeAddr函数被用来生成两个有标签的测试账户地址,这些地址可以在测试中用于与智能合约交互。

3.toString( )函数解释:

这段代码展示了如何在Solidity智能合约中实现两个函数:一个用于将uint256类型的数字转换为字符串,另一个用于获取一个存储在合约中的地址列表。以下是对这两个函数的详细解释:
这个函数用于将uint256类型的数字转换为字符串。这对于在智能合约中需要将数字以文本形式展示或记录时非常有用。

function toString(uint256 value) internal pure returns (string memory) {
    // 处理0的情况
    if (value == 0) {
        return "0";
    }
    // 数字转换为字符串的逻辑
    uint256 temp = value;
    uint256 digits;
    while (temp != 0) {
        digits++;
        temp /= 10;
    }
    bytes memory buffer = new bytes(digits);
    while (value != 0) {
        digits -= 1;
        buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
        value /= 10;
    }
    return string(buffer);
}

逻辑解释

  • 首先,函数检查如果输入值为0,直接返回字符串"0"。
  • 然后,计算数字的位数(digits),通过不断除以10直到数字变为0。
  • 根据位数,分配一个bytes数组作为缓冲区。
  • 再次遍历数字,每次取余数(个位数),将其转换为字符并存储到缓冲区中,然后数字除以10。
  • 最后,将bytes数组转换为字符串并返回。

buffer[digits] = bytes1(uint8(48 + uint256(value % 10)))的作用

这行代码是Solidity中将uint256类型的数字转换为字符串的一部分。它的作用是将数字的最低位(个位)转换为对应的ASCII字符,并存储到一个bytes数组中。这是在实现数字到字符串转换的过程中非常关键的一步。下面是对这行代码的详细解释:

buffer[digits] = bytes1(uint8(48 + uint256(value % 10)))
  1. buffer[digits]

    • 这是bytes数组的一个索引位置,用于存储转换后的字符。digits变量表示当前应该填充的数组位置。
  2. uint256(value % 10)

    • 这是一个取模运算,用于获取value的最低位(个位)。例如,如果value是123,那么value % 10的结果是3。
  3. 48 + uint256(value % 10)

    • 这个表达式将个位数字转换为对应的ASCII码。在ASCII表中,数字字符’0’到’9’的编码是48到57。因此,通过将个位数字加48,可以得到相应的ASCII码。例如,3加上48得到的是51,即字符’3’的ASCII码。
  4. uint8(...)

    • 将计算结果转换为uint8类型,这是因为bytes1只能存储一个字节的数据,而uint8正好是一个字节的大小。
  5. bytes1(...)

    • uint8类型的值转换为bytes1类型,这样就可以将字符存储到bytes数组中。
      这行代码的作用是将数字的最低位转换为字符,并存储到bytes数组的适当位置。这个过程在一个循环中重复进行,每次循环都会将value除以10(去掉最低位),直到value变为0。这样,就可以从数字的最低位到最高位依次转换并存储,最终得到一个表示数字的字符串。
      假设value是123,digits是3(因为123是三位数),那么这个过程如下:
  6. 第一次循环:value % 10是3,所以计算48 + 3得到51,即字符’3’的ASCII码。将这个值存储到buffer[3]

  7. value更新为12,digits减1。

  8. 第二次循环:value % 10是2,所以计算48 + 2得到50,即字符’2’的ASCII码。将这个值存储到buffer[2]

  9. value更新为1,digits减1。

  10. 第三次循环:value % 10是1,所以计算48 + 1得到49,即字符’1’的ASCII码。将这个值存储到buffer[1]

  11. value变为0,循环结束。

最终,buffer中存储的内容是字符串"123"的ASCII表示。

优化与进一步提升

openzeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/Strings.sol#L24-L25库中,有自己定义的toString,

这段代码是Solidity智能合约中将uint256类型的数字转换为字符串的另一种实现方式。它使用了Solidity的内置函数和一些低级汇编来高效地进行转换。以下是对这段代码的详细解释:

function toString(uint256 value) internal pure returns (string memory) {
    unchecked {
        uint256 length = Math.log10(value) + 1;
        string memory buffer = new string(length);
        uint256 ptr;
        /// @solidity memory-safe-assembly
        assembly {
            ptr := add(buffer, add(32, length))
        }
        while (true) {
            ptr--;
            /// @solidity memory-safe-assembly
            assembly {
                mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
            }
            value /= 10;
            if (value == 0) break;
        }
        return buffer;
    }
}
  1. unchecked

    • 用于禁用算术溢出和下溢的检查,这在确定不会发生溢出的情况下可以节省gas。
  2. 计算字符串长度

    • uint256 length = Math.log10(value) + 1;
    • 使用Math.log10函数计算数字的位数,并加1以确保有足够的空间存储字符串的null终止符。
  3. 创建字符串缓冲区

    • string memory buffer = new string(length);
    • 分配一个字符串,长度为计算出的位数。
  4. 设置指针

    • uint256 ptr;
    • 定义一个指针变量,用于在字符串缓冲区中存储字符。
    • 使用汇编代码设置指针到字符串的末尾,这是为了从字符串的末尾开始填充字符,以便于从高位到低位构建数字字符串。
  5. 转换循环

    • while (true) { ... }
    • 一个无限循环,用于将数字的每一位转换为字符并存储到缓冲区中。
    • 在循环中,使用ptr--将指针向前移动,以便在正确的位置存储下一个字符。
  6. 汇编代码

    • assembly { mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) }
    • 使用低级汇编指令mstore8在内存中存储一个字节。这里它存储的是数字value除以10的余数(即当前最低位的数字),并将其转换为对应的ASCII字符。
  7. 更新数字

    • value /= 10;
    • 将数字除以10,准备处理下一位。
  8. 循环退出条件

    • if (value == 0) break;
    • 如果数字减少到0,表示所有位数都已处理完毕,退出循环。
Math.log10(value)算法

这段代码定义了一个名为 log10 的 Solidity 函数,用于计算一个 uint256 类型数值的以 10 为底的对数。这个函数通过连续除以 10 的幂来计算对数,直到数值小于 10。这种方法避免了使用浮点运算,因为 Solidity 只支持整数运算。以下是对这段代码的详细解释:

function log10(uint256 value) internal pure returns (uint256) {
    uint256 result = 0;
    unchecked {
        if (value >= 10 ** 64) {
            value /= 10 ** 64;
            result += 64;
        }
        if (value >= 10 ** 32) {
            value /= 10 ** 32;
            result += 32;
        }
        if (value >= 10 ** 16) {
            value /= 10 ** 16;
            result += 16;
        }
        if (value >= 10 ** 8) {
            value /= 10 ** 8;
            result += 8;
        }
        if (value >= 10 ** 4) {
            value /= 10 ** 4;
            result += 4;
        }
        if (value >= 10 ** 2) {
            value /= 10 ** 2;
            result += 2;
        }
        if (value >= 10 ** 1) {
            result += 1;
        }
    }
    return result;
}
  1. 初始化结果

    • uint256 result = 0; 初始化一个变量 result 来存储对数值。
  2. unchecked

    • unchecked 关键字用于禁用溢出检查,这在处理大数值时可以节省 gas。
  3. 连续除法和累加

    • 函数通过一系列 if 语句检查 value 是否大于或等于 10 的不同幂次方。
    • 如果是,将 value 除以相应的 10 的幂次方,并将结果加到 result 上。例如,如果 value >= 10 ** 64,则除以 10 ** 64 并将 result 增加 64。
  4. 返回结果

    • 最后,函数返回计算出的对数值。

这段代码的巧妙之处在于它避免了使用循环来计算数字的位数,而是通过一系列条件判断和整数除法来实现。这种方法利用了对数的数学性质,即一个数可以表示为10的幂次方乘以一个小于该基数的数(例如,( N = 10^m \times n ),其中 ( n < 10 ))。通过逐步确定 ( m ) 的值,我们可以确定数字 ( N ) 的位数。

  1. 效率:在 Solidity 中,循环(特别是涉及状态变化的循环)可能会非常昂贵,因为每个循环迭代都可能产生额外的 gas 成本。通过使用条件判断代替循环,这段代码减少了执行步骤,从而可能降低了 gas 消耗。

  2. 可读性:虽然这种方法在一开始可能看起来不那么直观,但它将问题分解成了更小、更易于理解的部分。每一段代码都清晰地表示了数字的一个特定范围,这有助于开发者快速把握代码的逻辑。

  3. 确定性:循环的迭代次数取决于输入值,这可能导致难以预测的执行时间和 gas 成本。通过使用条件判断,这段代码提供了一种更可预测的执行路径。

  • 代码首先检查数字是否大于或等于 ( 10^{64} ),如果是,则除以 ( 10^{64} ) 并将结果加到总数上。
  • 接着,它继续检查更小的幂次,如 ( 10^{32} )、( 10^{16} ) 等,直到 ( 10^{1} )。
  • 每次检查都缩小了数字的范围,并逐步构建起对数值。

假设我们要计算 ( N = 12345678901234567890 ) 的位数:

  1. ( N ) 大于 ( 10^{64} ),所以除以 ( 10^{64} ),结果为 ( 1.23456789 ),并将结果加 64。
  2. ( 1.23456789 ) 大于 ( 10^{32} ),所以除以 ( 10^{32} ),结果为 ( 0.012345678 ),并将结果加 32。
  3. 继续这个过程,直到 ( 10^{1} )。

最终,我们得到的结果将是 ( 19 ),这正是 ( N ) 的位数。这种方法展示了如何在 Solidity 中高效且巧妙地处理数值问题,通过避免循环并利用条件判断来实现。这种方法不仅提高了代码的执行效率,还增强了代码的可读性和可预测性。

mstore8的妙用

这段代码是一个在Solidity智能合约中用于将 uint256 类型的数字转换为字符串的循环部分。它使用了Solidity的内联汇编来直接与EVM(Ethereum虚拟机)的内存进行交互。以下是对这段代码的详细解释:

while (true) {
    ptr--;
    /// @solidity memory-safe-assembly
    assembly {
        mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
    }
    value /= 10;
    if (value == 0) break;
}
  1. 无限循环

    • while (true) 创建了一个无限循环,这意味着循环会一直执行,直到遇到 break 语句。
  2. 指针递减

    • ptr-- 将指针向前移动一个字节,以便在内存中从字符串的末尾开始填充字符。
  3. 内联汇编

    • assembly { mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) }
    • mstore8 是一个汇编指令,用于在给定的内存地址(ptr)存储一个字节(8位)的数据。
    • byte(mod(value, 10), HEX_DIGITS) 计算 value 除以 10 的余数,并将其转换为对应的ASCII字符。这里假设 HEX_DIGITS 是一个定义了数字到字符映射的数组或常量。
  4. 更新值

    • value /= 10value 除以 10,以移除最低位的数字,为处理下一位数字做准备。
  5. 退出条件

    • if (value == 0) break 检查 value 是否为 0,如果是,则退出循环。

这里的细节是8字节的十进制,在计算机科学中,当我们谈论“8字节十进制对应的uint256”时,我们通常是在讨论一个无符号整数(uint256)类型可以存储的最大十进制数值。在Solidity中,uint256 是一个无符号的 256 位整数类型,用于存储非常大的数值。 8字节十进制数值范围

一个“字节”通常指的是 8 位(bit),而在讨论数字的大小时,我们通常使用字节(byte)作为单位,1 个字节等于 8 位。对于 uint256 来说:

  • 1 字节(8 位)可以表示的最大无符号整数是 (2^8 - 1 = 255)(十进制)。
  • 同理,8 字节(64 位)可以表示的最大无符号整数是 (2^{64} - 1)。

因此,8 字节(64 位)可以表示的十进制数值范围是从 (0) 到 (18,446,744,073,709,551,615)。

二,solidity许多值值的生成

在Solidity中,你不能直接在合约状态变量声明时初始化动态数组的大小,因为Solidity是一种静态类型语言,它需要在编译时知道存储需求。但是,你可以在合约的构造函数或任何函数中后续初始化和填充数组。以下是如何在合约中声明一个 uint256[] 类型的 rewards 数组,并在合约的构造函数中批量生成10个元素的示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract RewardsManager {
    uint256[] public rewards;

    constructor() {
        // 批量生成10个奖励并添加到数组中
        for (uint256 i = 0; i < 10; i++) {
            rewards.push(uint256(i + 1) * 100); // 例如,生成1到10的100倍的奖励
        }
    }
}

在这个示例中,我们使用了一个循环来填充 rewards 数组。在每次循环迭代中,我们调用 push 方法将一个新的 uint256 值添加到数组的末尾。这里,我们简单地生成了从100到1000(1100到10100)的奖励值。

其他生成数组的方法

如果你事先知道要生成的元素的值,你也可以使用Solidity的构造函数直接初始化数组:

pragma solidity ^0.8.0;

contract RewardsManager {
    uint256[] public rewards;

    constructor() {
        rewards = new uint256[](10);
        for (uint256 i = 0; i < 10; i++) {
            rewards[i] = uint256(i + 1) * 100;
        }
    }
}

这种方法首先使用 new uint256[](10) 创建一个具有10个元素的数组,然后使用循环填充每个元素的值。

注意事项

  • 在Solidity中,数组的 push 方法会将元素添加到数组的末尾,并自动调整数组的大小。这意味着你不需要预先知道数组的大小,但这也意味着使用 push 方法可能会稍微增加gas成本,因为它需要动态调整数组大小。
  • 如果你使用数组的直接初始化方法(如第二个示例),则在构造函数中直接设置数组的大小和值。这种方法在某些情况下可能更高效,因为它避免了动态调整数组大小的需要。

选择哪种方法取决于你的具体需求和偏好。如果你需要动态地添加元素,使用 push 方法可能更合适。如果你在初始化时就知道所有元素的值,直接初始化数组可能更有效。

  • 25
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值