diff --git a/src/utils/LibBit.sol b/src/utils/LibBit.sol index 336dfb1897..43683255f1 100644 --- a/src/utils/LibBit.sol +++ b/src/utils/LibBit.sol @@ -190,6 +190,30 @@ library LibBit { } } + /// @dev hex"ABCD" -> hex"0A0B0C0D". + function toNibbles(bytes memory s) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + let n := mload(s) + mstore(result, add(n, n)) // Store the new length. + s := add(s, 0x20) + let o := add(result, 0x20) + // forgefmt: disable-next-item + for { let i := 0 } lt(i, n) { i := add(i, 0x10) } { + let x := shr(128, mload(add(s, i))) + x := and(0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff, or(shl(64, x), x)) + x := and(0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff, or(shl(32, x), x)) + x := and(0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff, or(shl(16, x), x)) + x := and(0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff, or(shl(8, x), x)) + mstore(add(o, add(i, i)), + and(0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f, or(shl(4, x), x))) + } + mstore(add(o, add(s, s)), 0) // Zeroize slot after result. + mstore(0x40, add(0x40, add(o, add(s, s)))) // Allocate memory. + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* BOOLEAN OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/test/LibBit.t.sol b/test/LibBit.t.sol index 72fc4b037b..b3fcd5c5a5 100644 --- a/test/LibBit.t.sol +++ b/test/LibBit.t.sol @@ -339,4 +339,41 @@ contract LibBitTest is SoladyTest { } return s; } + + function testToNibblesGas() public { + bytes memory s = hex"123456789abcdef123456789abcdef123456789abcdef123456789abcdef"; + bytes memory expected = + hex"0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f"; + assertEq(LibBit.toNibbles(s), expected); + } + + function testToNibblesDifferential(uint256 r, bytes memory s) public { + if (r & 0x01 == 0) { + _brutalizeMemory(); + _misalignFreeMemoryPointer(); + } + bytes memory computed = LibBit.toNibbles(s); + _checkMemory(computed); + assertEq(computed, _toNibblesOriginal(s)); + } + + // Original code from Optimism (MIT-licensed): https://github.com/ethereum-optimism/optimism/blob/1bfc93f7c1fe1846217795a1f6051e1b0260f597/packages/contracts-bedrock/src/libraries/Bytes.sol#L94 + function _toNibblesOriginal(bytes memory input) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + let bytesLength := mload(input) + let nibblesLength := shl(0x01, bytesLength) + mstore(0x40, add(result, and(not(0x1f), add(nibblesLength, 0x3f)))) + mstore(result, nibblesLength) + let bytesStart := add(input, 0x20) + let nibblesStart := add(result, 0x20) + for { let i := 0x00 } lt(i, bytesLength) { i := add(i, 0x01) } { + let offset := add(nibblesStart, shl(0x01, i)) + let b := byte(0x00, mload(add(bytesStart, i))) + mstore8(offset, shr(0x04, b)) + mstore8(add(offset, 0x01), and(b, 0x0F)) + } + } + } }