go get github.com/forkcloser/blake3
blake3 implements the BLAKE3 cryptographic hash function.
The implementation aims to be performant without sacrificing (too much)
readability. Requires Go 1.25 or later.
In addition to the pure-Go implementation, this package also contains AVX-512
and AVX2 routines (generated by avo)
that greatly increase performance for large inputs and outputs on amd64. Every
other architecture runs the pure-Go code.
Three packages:
blake3—hash.Hash(New,Sum256,Sum512,DeriveKey) and the seekable XOF (Hasher.XOF).bao— Bao verified streaming: combined and outboard encodings, slices, and chunk-group verification.guts— the low-level tree-hashing primitives the two packages above are built on, for callers that hash their own trees.
A friendly fork of lukechampine/blake3
(import path lukechampine.com/blake3), taken at upstream v1.4.1 — which is
also upstream's current master. The exported API is upstream's with two
additions (guts.CompressBlocksN, bao.MaxGroup) and one removal: the five
Bao* wrappers upstream kept in the root package after moving Bao to its
own package, already deprecated there, are gone — use package bao. For
everything else, switching is a change of import path.
What diverges:
Correctness. OutputReader.Seek to an offset that was not aligned to the
XOF's internal buffer produced corrupt output on the next Read; the buffer
now caches an absolute window of the stream. Seek rejects offsets that would
wrap. New panics on a negative size or a key of the wrong length instead of
producing an undefined state. In bao, offset+length overflow is rejected
in ExtractSlice, DecodeSlice and VerifyChunk; VerifyChunk and
DecodeSlice verify the root of an empty encoding, VerifyChunk counts
leaves correctly so its outboard-length check is exact, and the parent-merge
stack in compressGroup is sized for 2⁶⁴ bytes (upstream's could overflow).
Every bao function validates its group (a value outside [0, MaxGroup]
panics with a message instead of an unrelated runtime fault), and Encode
rejects a negative length before writing anything. On amd64 a short eigentree
is copied into a stack buffer before it reaches the SIMD kernels, which read a
full 16 KiB: the bytes past the input's length are no longer touched.
Performance. Hasher.Write schedules a large write's eigentrees by total
size — serial below 24 KiB of trees, concurrent above — and CompressEigentree deals
16 KiB groups out in NumCPU-capped runs, so a 1 MiB write no longer spawns 64
goroutines. Small or unaligned XOF reads compress only the blocks they need
(guts.CompressBlocksN); large reads run across CPUs above a threshold. Bao
encoding reuses one parent buffer instead of allocating per node.
Assurance. The eigentree fast path is pinned to the chunk-at-a-time
reference by a deterministic test (every shape to 64 chunks, at many starting
counters) and a fuzz target on the split pattern; CI fuzzes it on every run.
XOF seeks are tested at buffer-unaligned offsets and with block counters past
2³². The bao decoders are fuzzed on hostile encodings and slice bounds. The
amd64 assembly is generated from avo/gen.go, which lives in its own module
so avo never enters the library's dependency graph; just lint-generated
regenerates it and fails if the committed file differs, and the generated
file passes go vet on amd64 (upstream's does not: its kernels broadcast
scalar arguments straight from the stack frame, which vet's asmdecl rejects;
here they go through a register first). The module builds and passes its
tests on the go 1.25 floor declared in go.mod as well as on the pinned
toolchain; vet's stdversion check keeps the floor honest.
Contributions that make sense upstream should go upstream first; see the organization's CONTRIBUTING and SECURITY policies.
Upstream lukechampine/blake3 v1.4.1 (dd9ffb9, the fork point) and this
fork, measured 2026-09-07 with Go 1.26.5 on an Apple M5 Pro (darwin/arm64,
so the generic implementation; no SIMD paths run on this host). The two
were run interleaved, three rounds of go test -run '^$' -bench . -benchmem -count=2 each, and paired with benchstat; both use the same plain b.N
loop, since b.Loop alone costs ~2 ns per iteration and shows up as a false
4% on the 73 ns case. Lower is better; ~ means no statistically
significant difference. Upstream's write-size rows were noisier (up to
±51%) than the fork's (≤4%) in this run; the medians agree with two
earlier runs.
| Benchmark | upstream v1.4.1 | this fork | vs upstream |
|---|---|---|---|
Write (32 KiB via io.CopyN) |
912.0 ns | 809.7 ns | −11% |
XOF/64 |
1009 ns | 3.1 ns | −99.7% |
XOF/1024 |
1.004 µs | 1.006 µs | ~ |
XOF/65536 |
37.84 µs | 36.10 µs | −4.6% |
XOF/1048576 |
223.2 µs | 209.5 µs | −6.1% |
Sum256/64 |
73.11 ns | 72.96 ns | ~ |
Sum256/1024 |
1.034 µs | 1.030 µs | ~ |
Sum256/65536 |
44.61 µs | 38.83 µs | −13% |
Sum256/1048576 |
236.5 µs | 225.7 µs | −4.6% |
WriteSizes/4096 |
9.47 µs | 4.70 µs | −50% |
WriteSizes/8192 |
14.89 µs | 9.39 µs | −37% |
WriteSizes/16384 |
20.27 µs | 18.21 µs | −10% |
WriteSizes/24576 |
27.06 µs | 26.93 µs | ~ |
WriteSizes/32768 |
29.84 µs | 25.88 µs | −13% |
WriteSizes/49152 |
40.98 µs | 31.56 µs | −23% |
WriteSizes/65536 |
42.73 µs | 38.37 µs | −10% |
WriteSizes/131072 |
58.39 µs | 53.29 µs | −8.7% |
WriteSizes/1048576 |
234.0 µs | 223.6 µs | −4.4% |
Allocations per operation, where either side allocates at all:
| Benchmark | upstream v1.4.1 | this fork |
|---|---|---|
XOF/65536 |
58 | 9 |
XOF/1048576 |
70 | 37 |
Sum256/65536 |
30 | 20 |
Sum256/1048576 |
171 | 133 |
WriteSizes/4096 … /24576 |
9 – 18 | 2 – 3 |
WriteSizes/32768 … /1048576 |
20 – 170 | 10 – 132 |
The XOF/64 row is the OutputReader fix: upstream recomputes the block on
every Seek(0)+Read, this fork serves it from its window. WriteSizes is
this fork's benchmark (writesizes_bench_test.go), added because
BenchmarkWrite alone, at io.Copy's 32 KiB, hid a 27% regression at exactly
that size in an earlier version of the write scheduler; the same file
compiled against upstream produced the upstream column.
The figures below are upstream's, from its README, on its own hardware ("2020 MacBook Air (i5-7600K @ 3.80GHz)"). They predate every change in this fork and are kept only to show the relative shape of the three paths; the fork's amd64 kernels have not been re-measured.
BenchmarkSum256/64 120 ns/op 533.00 MB/s
BenchmarkSum256/1024 2229 ns/op 459.36 MB/s
BenchmarkSum256/65536 16245 ns/op 4034.11 MB/s
BenchmarkWrite 245 ns/op 4177.38 MB/s
BenchmarkXOF 246 ns/op 4159.30 MB/s
BenchmarkSum256/64 120 ns/op 533.00 MB/s
BenchmarkSum256/1024 2229 ns/op 459.36 MB/s
BenchmarkSum256/65536 31137 ns/op 2104.76 MB/s
BenchmarkWrite 487 ns/op 2103.12 MB/s
BenchmarkXOF 329 ns/op 3111.27 MB/s
BenchmarkSum256/64 120 ns/op 533.00 MB/s
BenchmarkSum256/1024 2229 ns/op 459.36 MB/s
BenchmarkSum256/65536 133505 ns/op 490.89 MB/s
BenchmarkWrite 2022 ns/op 506.36 MB/s
BenchmarkXOF 1914 ns/op 534.98 MB/s
There is no assembly routine for single-block compressions. This is most noticeable for ~1KB inputs.
Each assembly routine inlines all 7 rounds, causing thousands of lines of duplicated code. Ideally the routines could be merged such that only a single routine is generated for AVX-512 and AVX2, without sacrificing too much performance.
On amd64, the SIMD kernels always process a full 16-chunk (16 KiB) buffer: a shorter eigentree tail is padded and the unused lanes' work discarded.