Skip to content

Instantly share code, notes, and snippets.

@bogdan
Created December 6, 2024 19:47
Show Gist options
  • Save bogdan/f6029ed94afce0b5262ad1b82f93f85e to your computer and use it in GitHub Desktop.
Save bogdan/f6029ed94afce0b5262ad1b82f93f85e to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
library IpfsCid {
bytes internal constant BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
/// @notice Converts a CID string to a `bytes32` hash.
/// @param cidString The Base32-encoded CID string (e.g., "bafy...").
/// @return hash The `bytes32` representation of the multihash digest.
function cidToHash(string memory cidString) internal pure returns (bytes32) {
bytes memory cidBytes = IpfsCid.decodeBase32(bytes(IpfsCid.dropFirstByte(cidString)));
// CID must have at least 36 bytes: 1-byte version, 1-byte codec, 34-byte multihash
require(cidBytes.length >= 36, "Invalid CID length");
// Validate CIDv1 and DAG-PB codec
require(cidBytes[0] == 0x01 && cidBytes[1] == 0x70, "Invalid CID prefix");
// Validate SHA-256 multihash prefix
require(cidBytes[2] == 0x12 && cidBytes[3] == 0x20, "Invalid multihash prefix");
// Extract and return the 32-byte digest
bytes32 digest;
assembly {
digest := mload(add(cidBytes, 36)) // Load 32 bytes starting from the 5th byte
}
return digest;
}
/// @dev Encodes arbitrary bytes into a Base32 string.
/// @param data The input bytes.
/// @return base32 The Base32 encoded string.
function toBase32(bytes memory data) internal pure returns (string memory) {
uint256 dataLength = data.length;
uint256 encodedLength = (dataLength * 8 + 4) / 5; // Base32 expands data by ~1.6x
bytes memory result = new bytes(encodedLength);
uint256 buffer;
uint256 bits;
uint256 index = 0;
for (uint256 i = 0; i < dataLength; i++) {
buffer = (buffer << 8) | uint8(data[i]);
bits += 8;
while (bits >= 5) {
bits -= 5;
result[index++] = bytes1(
BASE32_ALPHABET[(buffer >> bits) & 0x1F]
);
}
}
if (bits > 0) {
result[index++] = bytes1(
BASE32_ALPHABET[(buffer << (5 - bits)) & 0x1F]
);
}
return string(result);
}
/// @dev Decodes a Base32-encoded string into bytes.
/// @param input The Base32 string to decode.
/// @return The decoded bytes.
function decodeBase32(bytes memory input) internal pure returns (bytes memory) {
bytes memory inputBytes = bytes(input);
uint256 inputLength = inputBytes.length;
require(inputLength > 0, "Input string is empty");
// Base32 expands data by 8/5, so decoded length is inputLength * 5 / 8 (rounded up)
uint256 decodedLength = (inputLength * 5 + 7) / 8;
bytes memory result = new bytes(decodedLength);
uint256 buffer = 0;
uint256 bits = 0;
uint256 index = 0;
for (uint256 i = 0; i < inputLength; i++) {
uint256 value = indexOf(BASE32_ALPHABET, inputBytes[i]);
require(value < 32, "Invalid Base32 character");
buffer = (buffer << 5) | value;
bits += 5;
if (bits >= 8) {
bits -= 8;
result[index++] = bytes1(uint8(buffer >> bits));
}
}
return result;
}
function dropFirstByte(string memory input) internal pure returns (string memory) {
return string(dropFirstByte(bytes(input)));
}
function dropFirstByte(bytes memory input) internal pure returns (bytes memory) {
require(input.length > 0, "Input is empty");
// Create a new bytes array of length input.length - 1
bytes memory result = new bytes(input.length - 1);
// Copy all bytes except the first one
for (uint256 i = 1; i < input.length; i++) {
result[i - 1] = input[i];
}
return result;
}
/// @dev Finds the index of a character in the Base32 alphabet.
/// @param alphabet The Base32 alphabet.
/// @param char The character to find.
/// @return The index of the character in the alphabet.
function indexOf(bytes memory alphabet, bytes1 char) internal pure returns (uint256) {
for (uint256 i = 0; i < alphabet.length; i++) {
if (alphabet[i] == char) {
return i;
}
}
revert("Character not in Base32 alphabet");
}
/// @notice Converts a `bytes32` hash to a IPFS CIDv1 Base32 string.
/// @param hash The SHA-256 hash in `bytes32` format.
/// @return cid The CIDv1 Base32 string.
function bytes32ToCid(bytes32 hash) internal pure returns (string memory) {
// Multihash prefix for SHA-256: 0x12 (hash function), 0x20 (length of digest = 32 bytes)
bytes memory multihash = abi.encodePacked(hex"1220", hash);
// CIDv1 prefix for dag-pb: 0x01 (CIDv1), 0x70 (codec dag-pb)
bytes memory cidv1 = abi.encodePacked(hex"0170", multihash);
// Encode the CIDv1 bytes into Base32
return string.concat("b", IpfsCid.toBase32(cidv1));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment