Precompile Summary
| Address | Name | Gas | Input | Output | Behavior |
|---|---|---|---|---|---|
| 0x09 | BLAKE2F | rounds (first 4 bytes of input, big-endian uint32) × 1 — i.e., gas equals the number of compression rounds | Exactly 213 bytes: rounds (4 bytes, big-endian uint32), h (64 bytes, state vector), m (128 bytes, message block), t_0 (8 bytes, offset counter low), t_1 (8 bytes, offset counter high), f (1 byte, finalization flag: 0 or 1) | 64 bytes (updated state vector h after one compression) | Executes the BLAKE2b compression function F. This is not a complete hash: callers must implement full BLAKE2b (padding, counter updates across blocks, correct initial h, finalization) by iterating F. Introduced at Istanbul (EIP-152) mainly for Ethereum–Zcash interoperability (e.g., Equihash PoW verification). |
Threat Surface
BLAKE2F exposes a low-level cryptographic primitive on-chain: a single invocation runs one BLAKE2b compression round schedule controlled entirely by caller-supplied state (h, m), counters (t_0, t_1), round count, and finalization bit (f). Unlike precompiles that implement end-to-end algorithms (for example, SHA256 over arbitrary-length input), BLAKE2F shifts all algorithmic correctness — padding, byte counting, last-block handling, and parameter initialization — to the smart contract or off-chain prover. That split is the dominant security story: a “working” integration can still be cryptographically meaningless if any BLAKE2b construction rule is violated.
The surface also has economic and operational dimensions. Gas is linear in a 32-bit rounds field chosen by the caller, which is unlike most precompiles (fixed gas or length-based pricing). That makes estimation, forwarding gas, and griefing first-class concerns whenever rounds is even partly user-influenced. Finally, the precompile has seen relatively low mainnet use (mostly Zcash-related paths), so implementations and audits encounter fewer shared test vectors and production footguns than SHA256 or ECRECOVER, increasing the odds that client or application bugs persist longer.
Smart Contract Threats
T1: Gas Cost Manipulation via Round Count (Medium)
- Unbounded nominal gas from
rounds. Gas charged equals theroundsvalue interpreted as a big-endianuint32. An attacker can chooserounds = 0xFFFFFFFF(4,294,967,295), implying ~4.3 billion gas for the precompile alone — far beyond any block gas limit, so the transaction fails rather than draining the chain. The practical risk is targeted griefing and brittle integrations, not network-wide execution of that call. - User-controlled
roundswithout validation. If a contract forwards calldata or memory from an untrusted party into BLAKE2F without bounding or whitelistingrounds, callers can force reverts or exhaust gas budgets in nested calls, breaking liveness or enabling denial-of-service against dependent workflows. - Fixed gas budgets on
staticcall. Contracts that cap gas forwarded to0x09assuming a small constant number of rounds can fail intermittently when input supplies a largerrounds, causing unexpected reverts or failed proof verification even when the rest of the logic is sound.
Why it matters: BLAKE2F ties execution cost to a single attacker-controlled scalar. Misconfigured verification or parsing pipelines turn that knob into a reliable griefing primitive and makes gas estimation harder than for length-priced precompiles.
T2: Incorrect BLAKE2b Construction (High)
- Compression-only primitive. BLAKE2F is the internal F function, not BLAKE2b hashing. A correct caller must implement message padding (last block padded to 128 bytes per spec), maintain total bytes hashed in
t_0/t_1, setf = 1only on the last block, and initializeh(and other BLAKE2b parameters) per the standard. Omitting or reordering these steps yields deterministic but wrong “hashes.” - False sense of security. Wrong constructions can still produce fixed-length outputs that look like hashes in Solidity (e.g., truncated or compared as
bytes32). Downstream assumptions — uniqueness, collision resistance, binding to a specific message — can fail without any revert from the precompile. - Interop and proof verification. Systems verifying Zcash Equihash or other BLAKE2b-based witnesses must match exact serialization and block splitting. Any deviation accepts invalid proofs or rejects valid ones, depending on how checks are written.
Why it matters: The protocol gives you a correct F; it does not give you a correct BLAKE2b. Application-level mistakes silently destroy cryptographic meaning, which is especially severe when BLAKE2F gates proof acceptance or cross-chain claims.
T3: Non-Aligned Input Data Bug (Implementation / Historical) (Medium)
- Client memory layout sensitivity. EthereumJS had a consensus-relevant bug where BLAKE2F could return incorrect results when the 213-byte input was not laid out at offset 0 of the backing
Uint8Arrayslice.DataViewwas constructed without accounting forbyteOffset, so reads could target the wrong physical bytes, corrupting both gas accounting (from theroundsfield) and the cryptographic computation. - Cross-client divergence risk. Any bug of this class — interpreting buffer slices incorrectly — can produce fork-level disagreement between nodes until fixed and adopted network-wide.
Why it matters: Even a “pure” precompile is only trustworthy if the VM’s memory and buffer plumbing matches the spec on every client. Slice/offset bugs are easy to introduce in managed-language EVM implementations and hard to catch without dedicated differential tests.
T4: Finalization Flag Misuse (Medium)
- Domain of
f. The final byte must be 0 or 1. Iff > 1, the call fails and returns empty output (no 64-byte result), consistent with other precompile failure modes. f = 0on the last block. Usingf = 0on what should be the terminating compression yields an intermediate state, not a BLAKE2b output. Verifiers that treat that state as a final digest can accept non-finalized or malleable representations, depending on surrounding logic.- Proof systems. Equihash and similar constructions depend on exact BLAKE2b semantics. Finalization errors break the mapping between on-chain F invocations and the intended hash.
Why it matters: f is a single byte but encodes algorithm phase. Getting it wrong is a semantic bug with no cryptographic safety net: the precompile faithfully runs F; it does not know whether your block was truly “last.”
Protocol-Level Threats
P1: EthereumJS Consensus Bug from Input Data Misalignment (PR #3201, December 2023)
DataView initialization in the TypeScript EVM did not account for byteOffset when reading the BLAKE2F input from a subarray of contract memory. Depending on where the 213 bytes lived in the underlying buffer, clients could compute different outputs and gas than spec-conforming implementations. The issue was fixed in ethereumjs/ethereumjs-monorepo#3201.
P2: Unusual Gas Model — Cost Tied to rounds, Not Input Length
Most precompiles use fixed gas or pricing derived from input size. BLAKE2F’s gas is rounds × 1, independent of how “heavy” the 128-byte message block is in a bitwise sense. That complicates static gas analysis, EIP-1559 estimation, and layer-2 metering assumptions that treat precompiles as predictable from calldata length alone.
P3: Low Usage and Weaker Shared Scrutiny
Primary motivation was Zcash interoperability; on-chain call volume is low compared to SHA256 or identity-style precompiles. Fewer production paths means fewer public audits, fuzz corpora, and incident lessons, so both client and application edge cases may remain latent longer than for heavily exercised precompiles.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
rounds = 0 | 0 gas; returns input h unchanged (no compression work) | Callers must not assume “non-trivial hashing” occurred; can be abused to waste logic elsewhere or to bypass assumed work if higher layers misinterpret the outcome |
rounds = 0xFFFFFFFF | Gas requirement ~4.3B; exceeds block limit | Transaction cannot succeed on mainnet; usable for griefing if a contract surfaces untrusted rounds |
f = 0 | Non-final compression; intermediate h | Must not be treated as BLAKE2b output; verifiers can accept wrong PoW or proof material if they equate intermediate state with a digest |
f = 1 | Final compression for that block | Required on the true last block per BLAKE2b; omitting f = 1 on earlier duplicate-looking paths breaks standard semantics |
f > 1 | Call fails; empty return | Solidity callers must check output length; treating empty as success can desynchronize verification |
| Input length ≠ 213 bytes | Call fails; empty return | Robust contracts must validate length before interpreting success |
| Input shorter than 213 bytes zero-padded by caller | Padding becomes part of h / m / counters | Silent wrong compression inputs — classic memory-padding bug class |
h all-zero (or other non-standard initial state) | Still a well-formed call if lengths and f are valid | Unusual parameterization may be invalid BLAKE2b at the algorithm level even though F executes |
Real-World Exploits
Exploit 1: EthereumJS BLAKE2F Misalignment (2023)
- Root cause: Incorrect
DataViewconstruction over aUint8Arrayview with non-zerobyteOffset, so BLAKE2F read misaligned bytes forrounds,h,m, and counter fields. This corrupted both gas derived fromroundsand the cryptographic state update. - Details: The bug manifested when BLAKE2F input bytes were not at the start of the underlying ArrayBuffer-backed buffer — a normal situation for subarrays and certain memory copy paths in JS VMs. Other clients using correct offset handling disagreed with affected EthereumJS versions on the same transaction trace.
- Precompile’s role: BLAKE2F’s specification is deterministic; the failure was entirely in client buffer wiring, not in the on-chain address or interface. Nonetheless, any incorrect F implementation is a consensus hazard.
- Impact: Potential chain split between fixed and unfixed nodes for transactions invoking
0x09under triggering layouts; incorrect local simulation for tools built on the buggy stack. - References:
- ethereumjs/ethereumjs-monorepo#3201 — fix for BLAKE2F
DataView/byteOffsethandling - EIP-152: BLAKE2 compression function
Fprecompile
- ethereumjs/ethereumjs-monorepo#3201 — fix for BLAKE2F
Attack Scenarios
Scenario A: Gas Griefing via User-Controlled rounds
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @notice Anti-pattern: forwards untrusted `rounds` into BLAKE2F without bounds.
* Attacker sets rounds = 0xFFFFFFFF to force huge gas requirement or revert.
*/
contract VulnerableBlake2fRouter {
address constant BLAKE2F = address(0x09);
function verifyWithUntrustedRounds(
bytes calldata untrusted213 // caller claims this is valid BLAKE2F input
) external view returns (bytes memory) {
require(untrusted213.length == 213, "len");
// VULNERABLE: first 4 bytes (rounds) are attacker-controlled
(bool ok, bytes memory out) = BLAKE2F.staticcall(untrusted213);
require(ok, "call");
return out;
}
}
// Attack: submit untrusted213 with rounds = 0xFFFFFFFF (big-endian).
// Staticcall requires ~4.29e9 gas → always fails under block gas limit,
// griefing any workflow that must complete this call.Scenario B: Incorrect BLAKE2b Construction — Missing Finalization on Last Block
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @notice Simplified sketch: single F invocation with f=0 even when this is the last block.
* Real BLAKE2b requires f=1 on the final block; otherwise h is NOT the hash output.
*/
contract WrongFinalizationExample {
address constant BLAKE2F = address(0x09);
function compressLastBlockWrong(
bytes memory h64,
bytes memory m128,
uint64 t0,
uint64 t1,
uint32 rounds
) internal view returns (bytes memory hOut) {
require(h64.length == 64 && m128.length == 128, "sizes");
bytes memory input = new bytes(213);
// rounds (BE)
input[0] = bytes1(uint8(rounds >> 24));
input[1] = bytes1(uint8(rounds >> 16));
input[2] = bytes1(uint8(rounds >> 8));
input[3] = bytes1(uint8(rounds));
// copy h, m
for (uint256 i; i < 64; i++) input[4 + i] = h64[i];
for (uint256 j; j < 128; j++) input[68 + j] = m128[j];
// t0, t1 little-endian in BLAKE2b; illustrative only
for (uint256 k; k < 8; k++) input[196 + k] = bytes1(uint8(t0 >> (8 * k)));
for (uint256 k; k < 8; k++) input[204 + k] = bytes1(uint8(t1 >> (8 * k)));
// VULNERABLE: last block but f = 0 (non-final)
input[212] = 0x00;
(bool ok, bytes memory out) = BLAKE2F.staticcall(input);
require(ok && out.length == 64, "blake2f");
return out;
}
}
// Impact: any equality check against an expected BLAKE2b digest will fail OR,
// if a custom verifier mistakenly treats intermediate h as final, invalid proofs
// could be accepted depending on off-chain expectations.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
T1: Gas / griefing via rounds | Hard-cap rounds to the BLAKE2b constant (12 for BLAKE2b) or to your protocol’s required count | require(rounds == 12, "rounds"); after parsing the first 4 bytes; reject unknown values |
| T1: Gas forwarding | Forward explicit gas to 0x09 based on known rounds | BLAKE2F.staticcall{gas: rounds + buffer}(input) after validating rounds; never trust user-supplied gas budgets for critical paths |
| T2: Wrong BLAKE2b | Reuse audited BLAKE2b implementations and test vectors | Prefer off-chain hashing with on-chain verification only where spec matches; if composing F on-chain, mirror RFC 7693 and Zcash reference tests byte-for-byte |
| T2: Proof verification | Pin exact serialization (endianness, padding, counter progression) | Differential-test your contract against a reference implementation; include negative tests with wrong padding and wrong f |
| T3: Client bugs | Run multi-client fuzzing / differential testing for 0x09 | Include non-zero memory offsets in VM tests; track security advisories for your execution client |
T4: f misuse | Encode f in a single type-safe path | Reject invalid flags: only allow f == 0 or f == 1 before staticcall; centralize last-block handling so f cannot be set incorrectly |
| Input length | Reject anything except 213 bytes | require(input.length == 213) before call; do not manually zero-pad shorter buffers into F |
Compiler/EIP-Based Protections
- EIP-152 (Istanbul): Defines the
0x09precompile interface, exact 213-byte layout, gas formula (rounds× 1), and the BLAKE2b F function semantics used by the EVM. - Solidity / high-level languages: No first-class
blake2fopcode; callers usestaticcall— compilers do not automatically enforce BLAKE2b construction rules. Use libraries and formalized specs rather than ad hoc byte assembly. - Testing culture: Prefer shared vectors from BLAKE2b and Zcash/Equihash references; add regression tests for empty output (failed precompile) vs 64-byte success.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Medium | Medium | Griefing-style patterns (oversized parameters causing failure) are common; BLAKE2F’s rounds field is an unusually direct gas amplifier |
| T2 | Smart Contract | High | Medium | Many “wrong hash construction” bugs in crypto integrations; BLAKE2F raises the baseline complexity because F is not a full hash |
| T3 | Protocol / Client | Medium | Low (post-fix) | EthereumJS BLAKE2F misalignment (PR #3201) demonstrated real consensus risk class |
| T4 | Smart Contract | Medium | Medium | Final-byte phase errors are a known footgun in multi-block hash APIs |
| P1 | Protocol | High | Low (patched) | Concrete client bug with consensus implications until remediated and deployed |
| P2 | Protocol | Low | N/A | Design characteristic: complicates estimation rather than directly stealing funds |
| P3 | Operational | Medium | N/A | Low mainnet utilization reduces exposure but also reduces shared scrutiny |
Related Precompiles
| Precompile | Relationship |
|---|---|
| SHA256 (0x02) | Full Merkle-Damgård hash over arbitrary-length input; contrasts with BLAKE2F exposing only a single BLAKE2b compression. Useful mental model: end-to-end hash vs internal round function. |
| RIPEMD160 (0x03) | Another full hash precompile; same high-level lesson — different construction rules and length semantics than BLAKE2F. |
| IDENTITY (0x04) | Often appears in data-prep pipelines; incorrect copying or slicing before BLAKE2F can feed wrong m or counters. |
| POINT_EVALUATION (0x0A) | Another cryptographic primitive precompile (polynomial evaluation at a point) used in modern verification stacks; like BLAKE2F, correct surrounding algebra and encoding is caller responsibility. |