blob: 915aa71008c9fb74f43acfbaf656d50aa936f893 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math';
import 'dart:typed_data';
import 'package:analyzer/src/utilities/hash/blake3.dart';
import 'package:test/test.dart';
import 'blake3_reference.dart';
void main() {
// Avoid dart:mirrors so the same suite can run in AOT as well as JIT.
group('Blake3', () {
late Blake3Test suite;
setUp(() => suite = Blake3Test());
test('official vectors', () => suite.test_officialVectors());
test(
'block and tree boundaries',
() => suite.test_blockAndTreeBoundaries(),
);
test('offset views', () => suite.test_offsetViews());
test(
'random and structured inputs',
() => suite.test_randomAndStructuredInputs(),
);
test('reuse and ownership', () => suite.test_reuseAndOwnership());
test('streaming official vectors', () => suite.test_streamingVectors());
test('streaming split boundaries', () => suite.test_streamingSplits());
test('streaming tree boundaries', () => suite.test_streamingTrees());
test('streaming input views', () => suite.test_streamingViews());
test('streaming lifecycle', () => suite.test_streamingLifecycle());
});
}
/// Unkeyed 32-byte prefixes from upstream BLAKE3 1.8.7 test vectors (CC0).
///
/// https://github.com/BLAKE3-team/BLAKE3/blob/f3149ec5bb5449af877ba20377a11008ff499fa2/test_vectors/test_vectors.json
///
/// Input byte i is i % 251. These fixed expectations are not generated by any
/// Dart implementation. Tests need neither native code nor an upstream checkout.
const _blake3Vectors = <int, String>{
0: 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262',
1: '2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213',
2: '7b7015bb92cf0b318037702a6cdd81dee41224f734684c2c122cd6359cb1ee63',
3: 'e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f',
4: 'f30f5ab28fe047904037f77b6da4fea1e27241c5d132638d8bedce9d40494f32',
5: 'b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2',
6: '06c4e8ffb6872fad96f9aaca5eee1553eb62aed0ad7198cef42e87f6a616c844',
7: '3f8770f387faad08faa9d8414e9f449ac68e6ff0417f673f602a646a891419fe',
8: '2351207d04fc16ade43ccab08600939c7c1fa70a5c0aaca76063d04c3228eaeb',
63: 'e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b',
64: '4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98',
65: 'de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee',
127: 'd81293fda863f008c09e92fc382a81f5a0b4a1251cba1634016a0f86a6bd640d',
128: 'f17e570564b26578c33bb7f44643f539624b05df1a76c81f30acd548c44b45ef',
129: '683aaae9f3c5ba37eaaf072aed0f9e30bac0865137bae68b1fde4ca2aebdcb12',
1023: '10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11',
1024: '42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7',
1025: 'd00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444',
2048: 'e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a',
2049: '5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b6879522563030',
3072: 'b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd2',
3073: '7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd3',
4096: '015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e969',
4097: '9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb995',
5120: '9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833',
5121: '628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff',
6144: '3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca205',
6145: 'f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f',
7168: '61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a',
7169: 'a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e7817',
8192: 'aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a63',
8193: 'bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b',
16384: 'f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde4',
31744: '62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47',
102400: 'bc3e3d41a1146b069abffad3c0d44860cf664390afce4d9661f7902e7943e085',
};
class Blake3Test {
final hasher = ScalarBlake3();
/// Checks empty input, and also lengths immediately before, at, and after 64-byte
/// block and 1024-byte chunk boundaries against the independent reference.
/// Chunk counts around powers of two exercise both complete and incomplete
/// trees, including reductions that merge several pending subtrees.
void test_blockAndTreeBoundaries() {
var lengths = {
0,
for (var block = 1; block <= 129; block++)
for (final delta in [-1, 0, 1]) block * 64 + delta,
for (final chunks in [
for (var i = 1; i <= 20; i++) i,
31,
32,
33,
63,
64,
65,
127,
128,
129,
255,
256,
257,
511,
512,
513,
])
for (final delta in [-1, 0, 1]) chunks * 1024 + delta,
};
var random = Random(538901);
var bytes = _randomBytes(random, lengths.reduce(max));
for (var length in lengths) {
_check(Uint8List.sublistView(bytes, 0, length));
}
}
void test_officialVectors() {
for (var entry in _blake3Vectors.entries) {
var input = Uint8List.fromList([
for (var i = 0; i < entry.key; i++) i % 251,
]);
var expected = _decodeHex(entry.value);
expect(
referenceBlake3(input),
expected,
reason: 'reference ${entry.key}',
);
_check(input, expected: expected);
}
}
/// Hashes the same bytes at offsets 0..31 to cover aligned and unaligned views.
/// Each view must match the digest of a standalone input, excluding the
/// surrounding sentinel bytes. Comparing the entire backing buffer afterward
/// checks that neither the input nor the sentinel bytes were modified.
void test_offsetViews() {
var random = Random(29);
for (var length in [
0,
1,
63,
64,
65,
1023,
1024,
1025,
4095,
4096,
4097,
65537,
]) {
var bytes = _randomBytes(random, length);
var expected = referenceBlake3(bytes);
for (var offset = 0; offset < 32; offset++) {
var backing = Uint8List(length + 64)..fillRange(0, length + 64, 0xa5);
backing.setRange(offset, offset + length, bytes);
var before = Uint8List.fromList(backing);
_check(
Uint8List.sublistView(backing, offset, offset + length),
expected: expected,
);
expect(
backing,
before,
reason: 'input/sentinels $length offset $offset',
);
}
}
}
void test_randomAndStructuredInputs() {
var random = Random(20260920);
for (var i = 0; i < 64; i++) {
_check(_randomBytes(random, random.nextInt(20000)));
}
for (var length in [1, 65, 1024, 2049, 8192, 262145]) {
for (var value in [0, 0x80, 0xff]) {
_check(Uint8List(length)..fillRange(0, length, value));
}
}
}
/// Checks that one hasher can process differently sized, unmodifiable inputs
/// while keeping every returned digest independently owned. Empty and small
/// inputs after large inputs exercise the reset of reusable scratch storage.
/// Mutating the input buffers and making later hash calls must leave saved
/// digests intact. Mutating one digest must not affect other digests or the
/// result of a subsequent hash call.
void test_reuseAndOwnership() {
var random = Random(7);
var saved = <(Uint8List, Uint8List)>[];
for (var length in [524289, 0, 1, 1024, 63, 8193, 0, 65, 4096]) {
var bytes = _randomBytes(random, length);
var expected = referenceBlake3(bytes);
var digest = hasher.hash(bytes.asUnmodifiableView());
expect(digest, expected);
expect(digest.length, 32);
saved.add((digest, Uint8List.fromList(digest)));
bytes.fillRange(0, length, 0);
}
for (var (digest, expected) in saved) {
expect(digest, expected, reason: 'retained digest changed');
}
saved.last.$1.fillRange(0, 32, 0);
expect(hasher.hash(Uint8List(0)), _decodeHex(_blake3Vectors[0]!));
for (var (digest, expected) in saved.take(saved.length - 1)) {
expect(digest, expected, reason: 'digest storage aliases');
}
}
void test_streamingLifecycle() {
var sink = ScalarBlake3Sink();
var expected = referenceBlake3(Uint8List.fromList([1, 2, 3]));
var input = Uint8List.fromList([1, 2, 3]);
sink.add(input);
input.fillRange(0, input.length, 0);
var first = sink.finish();
expect(first, expected);
var saved = sink.finish();
first.fillRange(0, first.length, 0);
expect(sink.finish(), expected);
var empty = Uint8List(0);
var next = Uint8List.fromList([4]);
expect(() => sink.add(empty), throwsStateError);
expect(() => sink.add(next), throwsStateError);
for (var length in [65537, 0, 64, 1, 1024, 1025, 0]) {
sink.reset();
var bytes = Uint8List.fromList([
for (var i = 0; i < length; i++) i & 255,
]);
sink.add(bytes);
expect(sink.finish(), referenceBlake3(bytes));
expect(saved, expected);
}
sink.reset();
sink.add(Uint8List(4097));
sink.reset();
expect(sink.finish(), referenceBlake3(Uint8List(0)));
sink.reset();
sink.add(Uint8List.fromList([1, 2, 3]));
expect(sink.finish(), expected);
}
void test_streamingSplits() {
var sink = ScalarBlake3Sink();
var empty = Uint8List(0);
var random = Random(563962);
for (var length in [
0,
1,
63,
64,
65,
127,
128,
129,
1023,
1024,
1025,
2048,
2049,
]) {
var input = _randomBytes(random, length);
var expected = referenceBlake3(input);
for (var split = 0; split <= length; split++) {
sink.reset();
sink.add(Uint8List.sublistView(input, 0, split));
sink.add(empty);
sink.add(Uint8List.sublistView(input, split, length));
expect(sink.finish(), expected, reason: '$length split $split');
}
}
}
void test_streamingTrees() {
var random = Random(17);
var sink = ScalarBlake3Sink();
for (var chunks in [3, 4, 7, 8, 15, 16, 31, 32, 127, 128, 255, 256]) {
for (var delta in [-1, 0, 1]) {
var input = _randomBytes(random, chunks * 1024 + delta);
sink.reset();
for (var start = 0; start < input.length;) {
var end = min(input.length, start + 1 + random.nextInt(8192));
sink.add(Uint8List.sublistView(input, start, end));
start = end;
}
expect(sink.finish(), referenceBlake3(input));
}
}
}
void test_streamingVectors() {
var sink = ScalarBlake3Sink();
for (var entry in _blake3Vectors.entries) {
var input = Uint8List.fromList([
for (var i = 0; i < entry.key; i++) i % 251,
]);
var expected = _decodeHex(entry.value);
for (var size in [1, 3, 4, 63, 64, 65, 1023, 1024, 1025, 16384]) {
sink.reset();
for (var start = 0; start < input.length; start += size) {
sink.add(
Uint8List.sublistView(
input,
start,
min(start + size, input.length),
),
);
}
expect(sink.finish(), expected, reason: '${entry.key} pieces of $size');
}
}
}
void test_streamingViews() {
var random = Random(42);
for (var length in [0, 63, 64, 65, 1023, 1024, 1025, 4097]) {
var input = _randomBytes(random, length);
var expected = referenceBlake3(input);
for (var offset = 0; offset < 32; offset++) {
var backing = Uint8List(length + 64)..fillRange(0, length + 64, 0xa5);
backing.setRange(offset, offset + length, input);
var original = Uint8List.fromList(backing);
var view = Uint8List.sublistView(backing, offset, offset + length);
var sink = ScalarBlake3Sink();
sink.add(view.asUnmodifiableView());
expect(backing, original);
backing.fillRange(0, backing.length, 0);
expect(sink.finish(), expected);
sink.reset();
sink.add(
Uint8List.sublistView(
original.asUnmodifiableView(),
offset,
offset + length,
),
);
expect(sink.finish(), expected);
}
}
}
void _check(Uint8List input, {Uint8List? expected}) {
expected ??= referenceBlake3(input);
expect(
hasher.hash(input),
expected,
reason: 'length ${input.length}, offset ${input.offsetInBytes}',
);
}
Uint8List _decodeHex(String hex) => Uint8List.fromList([
for (var i = 0; i < hex.length; i += 2)
int.parse(hex.substring(i, i + 2), radix: 16),
]);
Uint8List _randomBytes(Random random, int length) =>
Uint8List.fromList(List.generate(length, (_) => random.nextInt(256)));
}