Răsfoiți Sursa

Optimize toString (#3573)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Igor Żuk 3 ani în urmă
părinte
comite
160bf1a6eb
4 a modificat fișierele cu 124 adăugiri și 57 ștergeri
  1. 1 0
      CHANGELOG.md
  2. 4 4
      contracts/mocks/StringsMock.sol
  3. 79 28
      contracts/utils/Strings.sol
  4. 40 25
      test/utils/Strings.test.js

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@
  * `Checkpoints`: Use procedural generation to support multiple key/value lengths. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
  * `Checkpoints`: Add new lookup mechanisms. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
  * `Array`: Add `unsafeAccess` functions that allow reading and writing to an element in a storage array bypassing Solidity's "out-of-bounds" check. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
+ * `Strings`: optimize `toString`. ([#3573](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3573))
 
 ### Breaking changes
 

+ 4 - 4
contracts/mocks/StringsMock.sol

@@ -5,19 +5,19 @@ pragma solidity ^0.8.0;
 import "../utils/Strings.sol";
 
 contract StringsMock {
-    function fromUint256(uint256 value) public pure returns (string memory) {
+    function toString(uint256 value) public pure returns (string memory) {
         return Strings.toString(value);
     }
 
-    function fromUint256Hex(uint256 value) public pure returns (string memory) {
+    function toHexString(uint256 value) public pure returns (string memory) {
         return Strings.toHexString(value);
     }
 
-    function fromUint256HexFixed(uint256 value, uint256 length) public pure returns (string memory) {
+    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
         return Strings.toHexString(value, length);
     }
 
-    function fromAddressHexFixed(address addr) public pure returns (string memory) {
+    function toHexString(address addr) public pure returns (string memory) {
         return Strings.toHexString(addr);
     }
 }

+ 79 - 28
contracts/utils/Strings.sol

@@ -7,48 +7,99 @@ pragma solidity ^0.8.0;
  * @dev String operations.
  */
 library Strings {
-    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
+    bytes16 private constant _SYMBOLS = "0123456789abcdef";
     uint8 private constant _ADDRESS_LENGTH = 20;
 
     /**
      * @dev Converts a `uint256` to its ASCII `string` decimal representation.
      */
     function toString(uint256 value) internal pure returns (string memory) {
-        // Inspired by OraclizeAPI's implementation - MIT licence
-        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
+        unchecked {
+            uint256 length = 1;
 
-        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;
+            // compute log10(value), and add it to length
+            uint256 valueCopy = value;
+            if (valueCopy >= 10**64) {
+                valueCopy /= 10**64;
+                length += 64;
+            }
+            if (valueCopy >= 10**32) {
+                valueCopy /= 10**32;
+                length += 32;
+            }
+            if (valueCopy >= 10**16) {
+                valueCopy /= 10**16;
+                length += 16;
+            }
+            if (valueCopy >= 10**8) {
+                valueCopy /= 10**8;
+                length += 8;
+            }
+            if (valueCopy >= 10**4) {
+                valueCopy /= 10**4;
+                length += 4;
+            }
+            if (valueCopy >= 10**2) {
+                valueCopy /= 10**2;
+                length += 2;
+            }
+            if (valueCopy >= 10**1) {
+                length += 1;
+            }
+            // now, length is 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), _SYMBOLS))
+                }
+                value /= 10;
+                if (value == 0) break;
+            }
+            return buffer;
         }
-        return string(buffer);
     }
 
     /**
      * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
      */
     function toHexString(uint256 value) internal pure returns (string memory) {
-        if (value == 0) {
-            return "0x00";
-        }
-        uint256 temp = value;
-        uint256 length = 0;
-        while (temp != 0) {
-            length++;
-            temp >>= 8;
+        unchecked {
+            uint256 length = 1;
+
+            // compute log256(value), and add it to length
+            uint256 valueCopy = value;
+            if (valueCopy >= 1 << 128) {
+                valueCopy >>= 128;
+                length += 16;
+            }
+            if (valueCopy >= 1 << 64) {
+                valueCopy >>= 64;
+                length += 8;
+            }
+            if (valueCopy >= 1 << 32) {
+                valueCopy >>= 32;
+                length += 4;
+            }
+            if (valueCopy >= 1 << 16) {
+                valueCopy >>= 16;
+                length += 2;
+            }
+            if (valueCopy >= 1 << 8) {
+                valueCopy >>= 8;
+                length += 1;
+            }
+            // now, length is log256(value) + 1
+
+            return toHexString(value, length);
         }
-        return toHexString(value, length);
     }
 
     /**
@@ -59,7 +110,7 @@ library Strings {
         buffer[0] = "0";
         buffer[1] = "x";
         for (uint256 i = 2 * length + 1; i > 1; --i) {
-            buffer[i] = _HEX_SYMBOLS[value & 0xf];
+            buffer[i] = _SYMBOLS[value & 0xf];
             value >>= 4;
         }
         require(value == 0, "Strings: hex length insufficient");

+ 40 - 25
test/utils/Strings.test.js

@@ -1,71 +1,86 @@
-const { constants, expectRevert } = require('@openzeppelin/test-helpers');
+const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
 
 const { expect } = require('chai');
 
 const StringsMock = artifacts.require('StringsMock');
 
 contract('Strings', function (accounts) {
-  beforeEach(async function () {
+  before(async function () {
     this.strings = await StringsMock.new();
   });
 
-  describe('from uint256 - decimal format', function () {
-    it('converts 0', async function () {
-      expect(await this.strings.fromUint256(0)).to.equal('0');
-    });
-
-    it('converts a positive number', async function () {
-      expect(await this.strings.fromUint256(4132)).to.equal('4132');
-    });
-
-    it('converts MAX_UINT256', async function () {
-      expect(await this.strings.fromUint256(constants.MAX_UINT256)).to.equal(constants.MAX_UINT256.toString());
-    });
+  describe('toString', function () {
+    for (const [ key, value ] of Object.entries([
+      '0',
+      '7',
+      '10',
+      '99',
+      '100',
+      '101',
+      '123',
+      '4132',
+      '12345',
+      '1234567',
+      '1234567890',
+      '123456789012345',
+      '12345678901234567890',
+      '123456789012345678901234567890',
+      '1234567890123456789012345678901234567890',
+      '12345678901234567890123456789012345678901234567890',
+      '123456789012345678901234567890123456789012345678901234567890',
+      '1234567890123456789012345678901234567890123456789012345678901234567890',
+    ].reduce((acc, value) => Object.assign(acc, { [value]: new BN(value) }), {
+      MAX_UINT256: constants.MAX_UINT256.toString(),
+    }))) {
+      it(`converts ${key}`, async function () {
+        expect(await this.strings.methods['toString(uint256)'](value)).to.equal(value.toString(10));
+      });
+    }
   });
 
-  describe('from uint256 - hex format', function () {
+  describe('toHexString', function () {
     it('converts 0', async function () {
-      expect(await this.strings.fromUint256Hex(0)).to.equal('0x00');
+      expect(await this.strings.methods['toHexString(uint256)'](0)).to.equal('0x00');
     });
 
     it('converts a positive number', async function () {
-      expect(await this.strings.fromUint256Hex(0x4132)).to.equal('0x4132');
+      expect(await this.strings.methods['toHexString(uint256)'](0x4132)).to.equal('0x4132');
     });
 
     it('converts MAX_UINT256', async function () {
-      expect(await this.strings.fromUint256Hex(constants.MAX_UINT256))
+      expect(await this.strings.methods['toHexString(uint256)'](constants.MAX_UINT256))
         .to.equal(web3.utils.toHex(constants.MAX_UINT256));
     });
   });
 
-  describe('from uint256 - fixed hex format', function () {
+  describe('toHexString fixed', function () {
     it('converts a positive number (long)', async function () {
-      expect(await this.strings.fromUint256HexFixed(0x4132, 32))
+      expect(await this.strings.methods['toHexString(uint256,uint256)'](0x4132, 32))
         .to.equal('0x0000000000000000000000000000000000000000000000000000000000004132');
     });
 
     it('converts a positive number (short)', async function () {
       await expectRevert(
-        this.strings.fromUint256HexFixed(0x4132, 1),
+        this.strings.methods['toHexString(uint256,uint256)'](0x4132, 1),
         'Strings: hex length insufficient',
       );
     });
 
     it('converts MAX_UINT256', async function () {
-      expect(await this.strings.fromUint256HexFixed(constants.MAX_UINT256, 32))
+      expect(await this.strings.methods['toHexString(uint256,uint256)'](constants.MAX_UINT256, 32))
         .to.equal(web3.utils.toHex(constants.MAX_UINT256));
     });
   });
 
-  describe('from address - fixed hex format', function () {
+  describe('toHexString address', function () {
     it('converts a random address', async function () {
       const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
-      expect(await this.strings.fromAddressHexFixed(addr)).to.equal(addr);
+      expect(await this.strings.methods['toHexString(address)'](addr)).to.equal(addr);
     });
 
     it('converts an address with leading zeros', async function () {
       const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
-      expect(await this.strings.fromAddressHexFixed(addr)).to.equal(addr);
+      expect(await this.strings.methods['toHexString(address)'](addr)).to.equal(addr);
     });
   });
 });