11

This may be simple in other languages but I can't figure out how to do it in Solidity.
I have a bytes32 like this 0x05416460deb76d57af601be17e777b93592d8d4d4a4096c57876a91c84f4a712.

I don't want to convert the bytes to a string, rather I just want to represent the whole thing as a string, like "0x05416460deb76d57af601be17e777b93592d8d4d4a4096c57876a91c84f4a712".
How can this be done in Solidity?

Update:
Why I need to do this: Basically I connect to an oracle, which does some work off-chain and finally uploads a file to IPFS. I need to get the content identifier into my contract from the oracle. The oracle can only send bytes32 as a response, so I convert it to a multihash and send only the digest as bytes32 from oracle to contract.
So far so good, I can recreate the multihash in my contract. The problem is that after this I create an ERC721 (NFT) token and I have to store some reference to the IPFS file in the metadata, which can only be in string format. This is where I'm stuck at the moment.

chnski
  • 557
  • 1
  • 4
  • 20
  • 1
    There's currently no easy way, because string is also a byte array. So you'd have to write a converter, that would create a 64-length byte array (that would later be converted to string) and fill it with each value somehow transformed to the ascii value representing the byte half. Example: half-byte `0` becomes `0x30`, half-byte `5` becomes `0x35`, half-byte `d` becomes `0x64`, etc. And then you can convert this new byte array to string... What is the reason behind the converting to string? Maybe a event log would be sufficient (so that an off-chain app could convert it more easily)? – Petr Hejda Jun 08 '21 at 22:00
  • Thanks for your answer! I updated my question to include why I have to do this. This indeed seems too complex for a relatively simple task, and I can only imagine how much gas this would consume... so I'm open to alternative solutions too. – chnski Jun 08 '21 at 22:21

2 Answers2

27

While the answer from @Burt looks correct (didn't test it though), there is a much more efficient way to solve the same task:

function toHex16 (bytes16 data) internal pure returns (bytes32 result) {
    result = bytes32 (data) & 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000 |
          (bytes32 (data) & 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> 64;
    result = result & 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000 |
          (result & 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> 32;
    result = result & 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 |
          (result & 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> 16;
    result = result & 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000 |
          (result & 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> 8;
    result = (result & 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> 4 |
          (result & 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> 8;
    result = bytes32 (0x3030303030303030303030303030303030303030303030303030303030303030 +
           uint256 (result) +
           (uint256 (result) + 0x0606060606060606060606060606060606060606060606060606060606060606 >> 4 &
           0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * 7);
}

function toHex (bytes32 data) public pure returns (string memory) {
    return string (abi.encodePacked ("0x", toHex16 (bytes16 (data)), toHex16 (bytes16 (data << 128))));
}

This code produces upper case output. For lower case output, just change 7 to 39 in the code.

Explanation

The idea is to process 16 bytes at once using binary operations.

The toHex16 function converts a sequence of 16 bytes represented as a bytes16 value into a sequence of 32 hexadecimal digits represented as a bytes32 value. The toHex function splits a bytes32 value into two bytes16 chunks, converts each chunk to hexadecimal representation via the toHex16 function, and finally concatenates the 0x prefix with the converted chunks using abi.encodePacked function.

The most sophisticated part is how the toHex16 function works. Let's explain it sentence by sentence.

The first sentence:

result = bytes32 (data) & 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000 |
      (bytes32 (data) & 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> 64;

Here we shift the last 64 bits of the input to the right by 64 bits, basically doing:

0123456789abcdeffedcba9876543210
\______________/\______________/
       |               |
       |               +---------------+
 ______V_______                  ______V_______
/              \                /              \
0123456789abcdef0000000000000000fedcba9876543210

The second sentence:

result = result & 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000 |
      (result & 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> 32;

Here we shift the last 32 bits of both 64-bit chunks to the right by 32 bits:

0123456789abcdef0000000000000000fedcba9876543210
\______/\______/                \______/\______/
   |       |                       |       |
   |       +-------+               |       +-------+
 __V___          __V___          __V___          __V___
/      \        /      \        /      \        /      \
012345670000000089abcdef00000000fedcba980000000076543210

The next sentence:

result = result & 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 |
      (result & 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> 16;

does:

012345670000000089abcdef00000000fedcba980000000076543210
\__/\__/        \__/\__/        \__/\__/        \__/\__/
 |   |           |   |           |   |           |   |
 |   +---+       |   +---+       |   +---+       |   +---+
 V_      V_      V_      V_      V_      V_      V_      V_
/  \    /  \    /  \    /  \    /  \    /  \    /  \    /  \
012300004567000089ab0000cdef0000fedc0000ba980000765400003210

And the next one:

result = result & 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000 |
      (result & 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> 8;

does:

012300004567000089ab0000cdef0000fedc0000ba980000765400003210
\/\/    \/\/    \/\/    \/\/    \/\/    \/\/    \/\/    \/\/
| |     | |     | |     | |     | |     | |     | |     | |
| +-+   | +-+   | +-+   | +-+   | +-+   | +-+   | +-+   | +-+
V   V   V   V   V   V   V   V   V   V   V   V   V   V   V   V
/\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\
01002300450067008900ab00cd00ef00fe00dc00ba00980076005400320010

The final sentence in this series is a bit different:

result = (result & 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> 4 |
      (result & 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> 8;

It shifts odd nibbles to the right by 4 bits, and even nibbles by 8 bits:

01002300450067008900ab00cd00ef00fe00dc00ba00980076005400320010
|\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\  |\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
 | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
 V V V V V V V V V V V V V V V V V V V V V V V V V V V V V V V V
000102030405060708090a0b0c0d0e0f0f0e0d0c0b0a09080706050403020100

So all the nibbles of the initial data are distributed one per byte.

Now with every byte x we need to do the following transformation:

x` = x < 10 ? '0' + x : 'A' + (x - 10)

Let's rewrite this formula a bit:

x` = ('0' + x) + (x < 10 ? 0 : 'A' - '0' - 10)
x` = ('0' + x) + (x < 10 ? 0 : 1) * ('A' - '0' - 10)

Note, that (x < 10 ? 0 : 1) could be calculated as ((x + 6) >> 4), thus we have:

x` = ('0' + x) + ((x + 6) >> 4) * ('A' - '0' - 10)
x` = (0x30 + x) + ((x + 0x06) >> 4) * 7

The final statement:

result = bytes32 (0x3030303030303030303030303030303030303030303030303030303030303030 +
       uint256 (result) +
       (uint256 (result) + 0x0606060606060606060606060606060606060606060606060606060606060606 >> 4 &
       0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * 7);

Basically performs the above calculation for every byte. The

0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F

mask after the right shift is needed to zero out the bits "dropped" by the right shift in the original formula.

BTW, it would be better to ask questions like this one at https://ethereum.stackexchange.com/

Mikhail Vladimirov
  • 13,572
  • 1
  • 38
  • 40
  • 1
    Amazing Job at explaining that. I cam confirm to everyone that this is working in Solidity v 0.8.7 . Other functions that use byte tend to fail even when you fix the deprecation error of byte --> bytes1 – 0xD1x0n Dec 31 '21 at 16:53
  • Such an ingenious transformation, beautiful! – Sebastian Hesse Feb 20 '22 at 21:26
4

Function bytes32ToString turns a bytes32 to hex string

function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) {
    uint8 i = 0;
    bytes memory bytesArray = new bytes(64);
    for (i = 0; i < bytesArray.length; i++) {

        uint8 _f = uint8(_bytes32[i/2] & 0x0f);
        uint8 _l = uint8(_bytes32[i/2] >> 4);

        bytesArray[i] = toByte(_f);
        i = i + 1;
        bytesArray[i] = toByte(_l);
    }
    return string(bytesArray);
}

function toByte(uint8 _uint8) public pure returns (byte) {
    if(_uint8 < 10) {
        return byte(_uint8 + 48);
    } else {
        return byte(_uint8 + 87);
    }
}
Burt
  • 1,520
  • 1
  • 12
  • 10