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/