Skip to content

Commit

Permalink
perf: Reduce function calls to save gas
Browse files Browse the repository at this point in the history
  • Loading branch information
dhl committed Oct 15, 2024
1 parent 2ebbf5b commit 4a06951
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 84 deletions.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function.
- [Features](#features)
- [Gas Usage](#gas-usage)
- [Testing](#testing)
- [Caveats](#caveats)
- [Acknowledgements](#acknowledgements)
- [References](#references)
- [License](#license)
Expand All @@ -36,20 +37,20 @@ Efforts such as [EIP-152](https://eips.ethereum.org/EIPS/eip-152)
and [Project Alchemy](https://github.com/Consensys/Project-Alchemy/tree/master/contracts/BLAKE2b) by Consensys have
attempted to provide a BLAKE2/BLAKE2b implementation. However, EIP-152 only provides a precompiled F compress function
instead of the full hash function, and Project Alchemy, which started before EIP-152, could not take advantage of the
precompile compress function, did not pass all reference implementation test vectors, and is no longer unmaintained.
precompile compress function, did not pass all reference implementation test vectors, and is no longer maintained.

`blake2b-solidity` aims to address these limitations by providing a high-performance, gas-efficient, and
feature-complete BLAKE2b implementation in Solidity, enabling developers to leverage the benefits of BLAKE2b directly
within Ethereum smart contracts.

## Features

1. Gas-efficient ⛽️ (See [Gas Usage](#gas-usage)).
2. Full support for variable input (tested to accept ~750KB of data given block gas limit of 30 million).
3. Full support for variable digest output size (1 up to 64 bytes).
4. Supports salting.
5. Supports personalized hashes.
6. Zero external dependency.
1. **Gas-efficient** ⛽️ (See [Gas Usage](#gas-usage)).
2. **Variable Input Support**: Accepts up to a theoretical maximum of 16 exbibytes ($2^{64}$ bytes,
see [Caveats](#caveats)).
4. **Salting**: Supports salting for added security.
5. **Personalized Hashes**: Supports personalized hashes.
6. **Zero External Dependency**: No external Solidity dependency. Only the EIP-152 precompiled contract is used.

## Gas Usage

Expand All @@ -62,11 +63,19 @@ were excluded in our benchmark.
| Hash Function | Implementation | Average Gas Cost | Digest Size (bits) | Relative Gas Cost (%) |
|------------------------|-----------------------|------------------|--------------------|-----------------------|
| Blake2b (Consensys) | Solidity | 255,427 | 512 | 1047% |
| Blake2b (this project) | Solidity + Precompile | 28,618 | 512 | 117% |
| Blake2b (this project) | Solidity + Precompile | 27,853 | 512 | 114% |
| ripemd160 | Native | 25,719 | 160 | 105% |
| sha256 | Native | 24,834 | 256 | 102% |
| keccak256 | Native | 24,400 | 256 | 100% |

## Caveats

1. Input size above 1 MiB ($2^{20}$ bytes) will likely fail due to block gas limit constraints (30 million gas at time
of writing).
2. Only the lower 64-bit of the `t` counter is implemented to save gas. This restricts the maximum supported input size
to $2^{64}$ bytes. The maximum allowed input size for BLAKE2b is $2^{128}$ bytes. Given the input size constraint
above, this is not going to be an issue.

## Testing

This project includes a comprehensive test suite to ensure strict conformance to the BLAKE2b specification.
Expand Down
143 changes: 67 additions & 76 deletions contracts/BLAKE2b.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ pragma solidity 0.8.27;
error OutputLengthCannotBeZero();
error OutputLengthExceeded();
error KeyLengthExceeded();
error InputLengthExceeded();

library BLAKE2b {
struct Context {
uint128 t; // processed bytes counter
uint128 c; // message block buffer counter
}

// Initial state vectors
//
// IV 0-3 as numerical values
Expand All @@ -31,44 +27,35 @@ library BLAKE2b {
bytes32 private constant IS0 = bytes32(hex"08c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5");
bytes32 private constant IS1 = bytes32(hex"d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b");

uint256 private constant BLOCK_SIZE = 128;

function hash(
bytes memory input,
bytes memory key,
bytes memory salt,
bytes memory personalization,
uint256 outlen
) internal view returns (bytes memory out) {
if (outlen == 0) {
uint256 digestLen
) internal view returns (bytes memory digest) {
if (digestLen == 0) {
revert OutputLengthCannotBeZero();
}

if (outlen > 64) {
if (digestLen > 64) {
revert OutputLengthExceeded();
}

if (key.length > 64) {
revert KeyLengthExceeded();
}

Context memory ctx;
out = new bytes(outlen);
////////////////////////////////////////////
// INIT
////////////////////////////////////////////

bytes memory state = init(ctx, outlen, key, salt, personalization);
update(ctx, state, input);
// See https://eips.ethereum.org/EIPS/eip-152#specification
bytes memory state = new bytes(213);

finalize(ctx, state, out, outlen);
}

function init(
Context memory ctx,
uint256 outlen,
bytes memory key,
bytes memory salt,
bytes memory person
) internal view returns (bytes memory state) {
// Initialize state by XORing initial state vectors with parameter block.
// Note the parameter block is broken up into different if statements to save gas.
bytes32[2] memory h = [IS0 ^ bytes32(outlen << 248), IS1];
bytes32[2] memory h = [IS0 ^ bytes32(digestLen << 248), IS1];

if (key.length > 0) {
h[0] ^= bytes32(key.length << 240);
Expand All @@ -78,77 +65,82 @@ library BLAKE2b {
h[1] ^= bytes32(salt);
}

if (person.length > 0) {
h[1] ^= bytes32(person) >> 128;
if (personalization.length > 0) {
h[1] ^= bytes32(personalization) >> 128;
}

// Copy state into the state buffer, encoded to the specification of EIP-152
state = new bytes(213);
assembly {
mstore8(add(state, 35), 12)
mcopy(add(state, 36), h, 64)
}

uint256 blockLen = 0;
uint256 buffLen = 0;

if (key.length > 0) {
update(ctx, state, key);
ctx.c = 128;
assembly {
let keyLen := mload(key)
mcopy(add(state, 100), add(key, 32), keyLen)
}
buffLen = BLOCK_SIZE;
}
}

function update(Context memory ctx, bytes memory state, bytes memory input) internal view {
uint128 t = ctx.t;
uint128 c = ctx.c;
uint256 inputOffset = 0;
uint256 inputLength = input.length;
////////////////////////////////////////////
// UPDATE
////////////////////////////////////////////

uint256 readInputOffset = 0;

// Read input in 128-byte chunks
while (inputOffset + 128 <= inputLength) {
// Read full block chunks
while (readInputOffset + BLOCK_SIZE <= input.length) {
// If the buffer is full, process it
if (c == 128) {
if (buffLen == BLOCK_SIZE) {
unchecked {
t += 128;
blockLen += BLOCK_SIZE;
}

bytes8[2] memory tt = [bytes8(reverseByteOrder(uint64(t))), bytes8(reverseByteOrder(uint64(t >> 64)))];
bytes8[1] memory tt = [bytes8(reverseByteOrder(uint64(blockLen)))];

assembly {
mcopy(add(state, 228), tt, 16)
mcopy(add(state, 228), tt, 8)
if iszero(staticcall(not(0), 0x09, add(state, 32), 0xd5, add(state, 36), 0x40)) {
revert(0, 0)
}
}

c = 0;
buffLen = 0;
}

assembly {
mcopy(add(add(state, 100), c), add(input, add(32, inputOffset)), 128)
mcopy(add(add(state, 100), buffLen), add(input, add(32, readInputOffset)), BLOCK_SIZE)
}

unchecked {
c = 128;
inputOffset += 128;
buffLen = BLOCK_SIZE;
readInputOffset += BLOCK_SIZE;
}
}

// Handle sub-128-byte chunk
if (inputOffset < inputLength) {
// Handle partial block
if (readInputOffset < input.length) {
// If the buffer is full, process it
if (c == 128) {
if (buffLen == BLOCK_SIZE) {
unchecked {
t += 128;
blockLen += BLOCK_SIZE;
}

bytes8[2] memory tt = [bytes8(reverseByteOrder(uint64(t))), bytes8(reverseByteOrder(uint64(t >> 64)))];
bytes8[1] memory tt = [bytes8(reverseByteOrder(uint64(blockLen)))];

assembly {
mcopy(add(state, 228), tt, 16)
mcopy(add(state, 228), tt, 8)
if iszero(staticcall(not(0), 0x09, add(state, 32), 0xd5, add(state, 36), 0x40)) {
revert(0, 0)
}
}

c = 0;
buffLen = 0;

// Reset the message buffer, as we are going to process a partial block
assembly {
mstore(add(state, 100), 0)
mstore(add(state, 132), 0)
Expand All @@ -157,40 +149,39 @@ library BLAKE2b {
}
}

// Safe casting, because left is always less than 128
uint128 left = uint128(inputLength - inputOffset);

assembly {
mcopy(add(add(state, 100), c), add(input, add(32, inputOffset)), left)
}

unchecked {
c += left;
// left = input.length - inputOffset. Safe casting, because left is always less than 128
let left := sub(mload(input), readInputOffset)
mcopy(add(add(state, 100), buffLen), add(input, add(32, readInputOffset)), left)
buffLen := add(buffLen, left)
}
}

ctx.t = t;
ctx.c = c;
}
////////////////////////////////////////////
// FINAL
////////////////////////////////////////////

function finalize(Context memory ctx, bytes memory state, bytes memory out, uint256 outlen) internal view {
uint128 t = ctx.t;
unchecked {
t += ctx.c;
blockLen += buffLen;
}

assembly {
mstore8(add(state, 244), true)
}

bytes8[2] memory tt = [bytes8(reverseByteOrder(uint64(t))), bytes8(reverseByteOrder(uint64(t >> 64)))];
bytes8[1] memory tt = [bytes8(reverseByteOrder(uint64(blockLen)))];

assembly {
mcopy(add(state, 228), tt, 16)
// Set final block flag
mstore8(add(state, 244), 1)
mcopy(add(state, 228), tt, 8)
if iszero(staticcall(not(0), 0x09, add(state, 32), 0xd5, add(state, 36), 0x40)) {
revert(0, 0)
}
mcopy(add(out, 32), add(state, 36), outlen)

// digest = new bytes(digestLen)
digest := mload(0x40)
mstore(0x40, add(digest, add(digestLen, 0x20)))
mstore(digest, digestLen)

// copy final hash state to digest
mcopy(add(digest, 32), add(state, 36), digestLen)
}
}

Expand Down
4 changes: 4 additions & 0 deletions contracts/BLAKE2bTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contract BLAKE2bTest {
return BLAKE2b.hash(input, key, salt, personalization, outlen);
}

// solc-disable-next-line
function callHash(
bytes memory input,
bytes memory key,
Expand All @@ -26,14 +27,17 @@ contract BLAKE2bTest {
return BLAKE2b.hash(input, key, salt, personalization, outlen);
}

// solc-disable-next-line
function callRipemd160(bytes memory input) public returns (bytes memory) {
return abi.encodePacked(ripemd160(input));
}

// solc-disable-next-line
function callSha256(bytes memory input) public returns (bytes memory) {
return abi.encodePacked(sha256(input));
}

// solc-disable-next-line
function callKeccak256(bytes memory input) public returns (bytes memory) {
return abi.encodePacked(keccak256(input));
}
Expand Down

0 comments on commit 4a06951

Please sign in to comment.