From b82c38f860637ab66d846c824e52686910277cc5 Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 13:28:01 +0100 Subject: [PATCH 1/6] try differnt benchmarking --- benchmarks/benchmark_xof.py | 264 ++++++++++++++++++++++++------------ 1 file changed, 176 insertions(+), 88 deletions(-) diff --git a/benchmarks/benchmark_xof.py b/benchmarks/benchmark_xof.py index 087b018..0ccd653 100644 --- a/benchmarks/benchmark_xof.py +++ b/benchmarks/benchmark_xof.py @@ -1,91 +1,179 @@ -import random from hashlib import shake_128 -from xoflib import Shaker128, Shaker256, TurboShaker128, TurboShaker256 -import time +from xoflib import Shaker128, Shaker256, TurboShaker128, TurboShaker256 +from timeit import timeit from Crypto.Hash.SHAKE128 import SHAKE128_XOF -random.seed(0) -t0 = time.time() -xof = Shaker128(b"123").finalize() -for _ in range(10_000): - n = random.randint(1, 500) - a = xof.read(n) -print(f"10_000 calls (read(1, 500)) with xoflib: {time.time() - t0 }") - -random.seed(0) -t0 = time.time() -for _ in range(10_000): - n = random.randint(1, 500) - a = shake_128(b"123").digest(n) -print(f"10_000 calls (read(1, 500)) with hashlib: {time.time() - t0 }") - -random.seed(0) -t0 = time.time() -xof = SHAKE128_XOF() -xof.update(b"123") -for _ in range(10_000): - n = random.randint(1, 500) - a = xof.read(n) -print(f"10_000 calls (read(1, 500)) with pycryptodome: {time.time() - t0 }") - -print("-" * 80) - -t0 = time.time() -xof = Shaker128(b"123").finalize() -for _ in range(1_000_000): - a = xof.read(1) -print(f"1_000_000 single byte reads with xoflib: {time.time() - t0 }") - -t0 = time.time() -xof = SHAKE128_XOF() -xof.update(b"123") -for _ in range(1_000_000): - a = xof.read(1) -print(f"1_000_000 single byte reads pycryptodome: {time.time() - t0 }") - -t0 = time.time() -xof = Shaker128(b"123").finalize() -for _ in range(1_000_000): - a = xof.read(168) -print(f"100_000 block reads with xoflib: {time.time() - t0 }") - -t0 = time.time() -xof = SHAKE128_XOF() -xof.update(b"123") -for _ in range(1_000_000): - a = xof.read(168) -print(f"100_000 block reads pycryptodome: {time.time() - t0 }") - -print("-" * 80) - -random.seed(0) -t0 = time.time() -xof = Shaker128(b"123").finalize() -for _ in range(10_000): - n = random.randint(1, 5000) - a = xof.read(n) -print(f"10_000 calls (read(1, 5000)) with xoflib Shake128: {time.time() - t0 }") - -random.seed(0) -t0 = time.time() -xof = Shaker256(b"123").finalize() -for _ in range(10_000): - n = random.randint(1, 5000) - a = xof.read(n) -print(f"10_000 calls (read(1, 5000)) with xoflib Shaker256: {time.time() - t0 }") - -random.seed(0) -t0 = time.time() -xof = TurboShaker128(1, b"123").finalize() -for _ in range(10_000): - n = random.randint(1, 5000) - a = xof.read(n) -print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker128: {time.time() - t0 }") - -random.seed(0) -t0 = time.time() -xof = TurboShaker256(1, b"123").finalize() -for _ in range(10_000): - n = random.randint(1, 5000) - a = xof.read(n) -print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker256: {time.time() - t0 }") + +def xor_bytes(a, b): + return bytes(i ^ j for i, j in zip(a, b)) + + +def benchmark_xoflib_stream(absorb, c, n): + xof = Shaker128(absorb).finalize() + res = bytes([0] * c) + for _ in range(n): + chunk = xof.read(c) + res = xor_bytes(res, chunk) + return res + +def benchmark_hashlib_one_call(absorb, c, n): + """ + Requires generating all c * n bytes in one go + """ + xof = shake_128(absorb).digest(c * n) + xof_chunks = [xof[i : i + c] for i in range(0, c * n, c)] + assert len(xof_chunks) == n + + res = bytes([0] * c) + for chunk in xof_chunks: + res = xor_bytes(res, chunk) + return res + +def benchmark_hashlib_stream(absorb, c, n): + """ + Requests only the bytes needed, but requires n calls to the digest + """ + res = bytes([0] * c) + for i in range(n): + chunk = shake_128(absorb).digest((i + 1) * c)[-c : ] + res = xor_bytes(res, chunk) + return res + +def benchmark_pycryptodome_stream(absorb, c, n): + xof = SHAKE128_XOF().update(absorb) + res = bytes([0] * c) + for _ in range(n): + chunk = xof.read(c) + res = xor_bytes(res, chunk) + return res + + +# Ensure things work +a = benchmark_xoflib_stream(b"benchmarking...", 123, 1000) +b = benchmark_hashlib_one_call(b"benchmarking...", 123, 1000) +c = benchmark_hashlib_stream(b"benchmarking...", 123, 1000) +d = benchmark_pycryptodome_stream(b"benchmarking...", 123, 1000) + +assert a == b == c == d + +print("="*80) +for (c, n) in [(1, 10_000), (100, 10_000), (1000, 1000), (10_000, 1000), (32, 1_000_000)]: + print(f"Requesting {c} bytes from XOF {n} times") + xoflib_time = timeit( + 'benchmark_xoflib_stream(b"benchmarking...", c, n)', + globals={"benchmark_xoflib_stream": benchmark_xoflib_stream, "c" : c, "n" : n}, + number = 10 + ) + print(f"xoflib: {xoflib_time:.2f}s") + + hashlib_single_time = timeit( + 'benchmark_hashlib_one_call(b"benchmarking...", c, n)', + globals={"benchmark_hashlib_one_call": benchmark_hashlib_one_call, "c" : c, "n" : n}, + number = 10 + ) + print(f"hashlib (single call): {hashlib_single_time:.2f}s") + + # TOO slow and annoying to benchmark + # hashlib_stream_time = timeit( + # 'benchmark_hashlib_stream(b"benchmarking...", c, n)', + # globals={"benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, + # number = 10 + # ) + # print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") + + pycryptodome_time = timeit( + 'benchmark_pycryptodome_stream(b"benchmarking...", c, n)', + globals={"benchmark_pycryptodome_stream": benchmark_pycryptodome_stream, "c" : c, "n" : n}, + number = 10 + ) + print(f"pycryptodome: {pycryptodome_time:.2f}s") + print("="*80) + + +# print("-" * 80) + +# random.seed(0) +# t0 = time.time() +# xof = Shaker128(b"123").finalize() +# for _ in range(10_000): +# n = random.randint(1, 500) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 500)) with xoflib: {time.time() - t0 }") + +# random.seed(0) +# t0 = time.time() +# for _ in range(10_000): +# n = random.randint(1, 500) +# a = shake_128(b"123").digest(n) +# print(f"10_000 calls (read(1, 500)) with hashlib: {time.time() - t0 }") + +# random.seed(0) +# t0 = time.time() +# xof = SHAKE128_XOF() +# xof.update(b"123") +# for _ in range(10_000): +# n = random.randint(1, 500) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 500)) with pycryptodome: {time.time() - t0 }") + +# print("-" * 80) + +# t0 = time.time() +# xof = Shaker128(b"123").finalize() +# for _ in range(1_000_000): +# a = xof.read(1) +# print(f"1_000_000 single byte reads with xoflib: {time.time() - t0 }") + +# t0 = time.time() +# xof = SHAKE128_XOF() +# xof.update(b"123") +# for _ in range(1_000_000): +# a = xof.read(1) +# print(f"1_000_000 single byte reads pycryptodome: {time.time() - t0 }") + +# t0 = time.time() +# xof = Shaker128(b"123").finalize() +# for _ in range(1_000_000): +# a = xof.read(168) +# print(f"100_000 block reads with xoflib: {time.time() - t0 }") + +# t0 = time.time() +# xof = SHAKE128_XOF() +# xof.update(b"123") +# for _ in range(1_000_000): +# a = xof.read(168) +# print(f"100_000 block reads pycryptodome: {time.time() - t0 }") + +# print("-" * 80) + +# random.seed(0) +# t0 = time.time() +# xof = Shaker128(b"123").finalize() +# for _ in range(10_000): +# n = random.randint(1, 5000) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 5000)) with xoflib Shake128: {time.time() - t0 }") + +# random.seed(0) +# t0 = time.time() +# xof = Shaker256(b"123").finalize() +# for _ in range(10_000): +# n = random.randint(1, 5000) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 5000)) with xoflib Shaker256: {time.time() - t0 }") + +# random.seed(0) +# t0 = time.time() +# xof = TurboShaker128(1, b"123").finalize() +# for _ in range(10_000): +# n = random.randint(1, 5000) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker128: {time.time() - t0 }") + +# random.seed(0) +# t0 = time.time() +# xof = TurboShaker256(1, b"123").finalize() +# for _ in range(10_000): +# n = random.randint(1, 5000) +# a = xof.read(n) +# print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker256: {time.time() - t0 }") From baee70c40c471acdddcff4434592a4025be598fa Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 13:40:39 +0100 Subject: [PATCH 2/6] add streaming hashlib API --- benchmarks/benchmark_xof.py | 22 +++++++++++----------- benchmarks/shake_wrapper.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 benchmarks/shake_wrapper.py diff --git a/benchmarks/benchmark_xof.py b/benchmarks/benchmark_xof.py index 0ccd653..53957d3 100644 --- a/benchmarks/benchmark_xof.py +++ b/benchmarks/benchmark_xof.py @@ -2,7 +2,7 @@ from xoflib import Shaker128, Shaker256, TurboShaker128, TurboShaker256 from timeit import timeit from Crypto.Hash.SHAKE128 import SHAKE128_XOF - +from shake_wrapper import shake_128_hashlib def xor_bytes(a, b): return bytes(i ^ j for i, j in zip(a, b)) @@ -34,8 +34,9 @@ def benchmark_hashlib_stream(absorb, c, n): Requests only the bytes needed, but requires n calls to the digest """ res = bytes([0] * c) - for i in range(n): - chunk = shake_128(absorb).digest((i + 1) * c)[-c : ] + xof = shake_128_hashlib(absorb) + for _ in range(n): + chunk = xof.read(c) res = xor_bytes(res, chunk) return res @@ -57,7 +58,7 @@ def benchmark_pycryptodome_stream(absorb, c, n): assert a == b == c == d print("="*80) -for (c, n) in [(1, 10_000), (100, 10_000), (1000, 1000), (10_000, 1000), (32, 1_000_000)]: +for (c, n) in [(1, 10_000), (100, 10_000), (1000, 1000), (10_000, 1000), (32, 100_000)]: print(f"Requesting {c} bytes from XOF {n} times") xoflib_time = timeit( 'benchmark_xoflib_stream(b"benchmarking...", c, n)', @@ -73,13 +74,12 @@ def benchmark_pycryptodome_stream(absorb, c, n): ) print(f"hashlib (single call): {hashlib_single_time:.2f}s") - # TOO slow and annoying to benchmark - # hashlib_stream_time = timeit( - # 'benchmark_hashlib_stream(b"benchmarking...", c, n)', - # globals={"benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, - # number = 10 - # ) - # print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") + hashlib_stream_time = timeit( + 'benchmark_hashlib_stream(b"benchmarking...", c, n)', + globals={"benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, + number = 10 + ) + print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") pycryptodome_time = timeit( 'benchmark_pycryptodome_stream(b"benchmarking...", c, n)', diff --git a/benchmarks/shake_wrapper.py b/benchmarks/shake_wrapper.py new file mode 100644 index 0000000..0ded839 --- /dev/null +++ b/benchmarks/shake_wrapper.py @@ -0,0 +1,25 @@ +from hashlib import shake_128 + +class ShakeStream: + """ + Written by David Buchanan + + Taken from: + https://github.com/pyca/cryptography/issues/9185#issuecomment-1868518432 + """ + def __init__(self, digestfn) -> None: + # digestfn is anything we can call repeatedly with different lengths + self.digest = digestfn + self.buf = self.digest(32) # arbitrary starting length + self.offset = 0 + + def read(self, n: int) -> bytes: + # double the buffer size until we have enough + while self.offset + n > len(self.buf): + self.buf = self.digest(len(self.buf) * 2) + res = self.buf[self.offset:self.offset + n] + self.offset += n + return res + +def shake_128_hashlib(absorb): + return ShakeStream(shake_128(absorb).digest) From e8bc7c0244177378cf3821d8f9b69815a8ec4e0c Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 14:23:12 +0100 Subject: [PATCH 3/6] rename xof in benchmark import --- benchmarks/benchmark_xof.py | 185 +++++++++++------------------------- benchmarks/shake_wrapper.py | 5 +- tests/test_xoflib.py | 10 +- xoflib.pyi | 4 +- 4 files changed, 64 insertions(+), 140 deletions(-) diff --git a/benchmarks/benchmark_xof.py b/benchmarks/benchmark_xof.py index 53957d3..1d80f1f 100644 --- a/benchmarks/benchmark_xof.py +++ b/benchmarks/benchmark_xof.py @@ -1,26 +1,27 @@ -from hashlib import shake_128 -from xoflib import Shaker128, Shaker256, TurboShaker128, TurboShaker256 from timeit import timeit +from hashlib import shake_128, shake_256 +from xoflib import Shake128, Shake256 from Crypto.Hash.SHAKE128 import SHAKE128_XOF -from shake_wrapper import shake_128_hashlib +from Crypto.Hash.SHAKE256 import SHAKE256_XOF +from shake_wrapper import shake_128_hashlib, shake_256_hashlib def xor_bytes(a, b): return bytes(i ^ j for i, j in zip(a, b)) -def benchmark_xoflib_stream(absorb, c, n): - xof = Shaker128(absorb).finalize() +def benchmark_xoflib_stream(shake, absorb, c, n): + xof = shake(absorb).finalize() res = bytes([0] * c) for _ in range(n): chunk = xof.read(c) res = xor_bytes(res, chunk) return res -def benchmark_hashlib_one_call(absorb, c, n): +def benchmark_hashlib_one_call(shake, absorb, c, n): """ Requires generating all c * n bytes in one go """ - xof = shake_128(absorb).digest(c * n) + xof = shake(absorb).digest(c * n) xof_chunks = [xof[i : i + c] for i in range(0, c * n, c)] assert len(xof_chunks) == n @@ -29,19 +30,20 @@ def benchmark_hashlib_one_call(absorb, c, n): res = xor_bytes(res, chunk) return res -def benchmark_hashlib_stream(absorb, c, n): +def benchmark_hashlib_stream(shake, absorb, c, n): """ Requests only the bytes needed, but requires n calls to the digest """ res = bytes([0] * c) - xof = shake_128_hashlib(absorb) + xof = shake(absorb) for _ in range(n): chunk = xof.read(c) res = xor_bytes(res, chunk) return res -def benchmark_pycryptodome_stream(absorb, c, n): - xof = SHAKE128_XOF().update(absorb) +def benchmark_pycryptodome_stream(shake, absorb, c, n): + shake.__init__() + xof = shake.update(absorb) res = bytes([0] * c) for _ in range(n): chunk = xof.read(c) @@ -50,130 +52,49 @@ def benchmark_pycryptodome_stream(absorb, c, n): # Ensure things work -a = benchmark_xoflib_stream(b"benchmarking...", 123, 1000) -b = benchmark_hashlib_one_call(b"benchmarking...", 123, 1000) -c = benchmark_hashlib_stream(b"benchmarking...", 123, 1000) -d = benchmark_pycryptodome_stream(b"benchmarking...", 123, 1000) +a = benchmark_xoflib_stream(Shake128, b"benchmarking...", 123, 1000) +b = benchmark_hashlib_one_call(shake_128, b"benchmarking...", 123, 1000) +c = benchmark_hashlib_stream(shake_128_hashlib, b"benchmarking...", 123, 1000) +d = benchmark_pycryptodome_stream(SHAKE128_XOF(), b"benchmarking...", 123, 1000) assert a == b == c == d -print("="*80) -for (c, n) in [(1, 10_000), (100, 10_000), (1000, 1000), (10_000, 1000), (32, 100_000)]: - print(f"Requesting {c} bytes from XOF {n} times") - xoflib_time = timeit( - 'benchmark_xoflib_stream(b"benchmarking...", c, n)', - globals={"benchmark_xoflib_stream": benchmark_xoflib_stream, "c" : c, "n" : n}, - number = 10 - ) - print(f"xoflib: {xoflib_time:.2f}s") - - hashlib_single_time = timeit( - 'benchmark_hashlib_one_call(b"benchmarking...", c, n)', - globals={"benchmark_hashlib_one_call": benchmark_hashlib_one_call, "c" : c, "n" : n}, - number = 10 - ) - print(f"hashlib (single call): {hashlib_single_time:.2f}s") - - hashlib_stream_time = timeit( - 'benchmark_hashlib_stream(b"benchmarking...", c, n)', - globals={"benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, - number = 10 - ) - print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") - - pycryptodome_time = timeit( - 'benchmark_pycryptodome_stream(b"benchmarking...", c, n)', - globals={"benchmark_pycryptodome_stream": benchmark_pycryptodome_stream, "c" : c, "n" : n}, - number = 10 - ) - print(f"pycryptodome: {pycryptodome_time:.2f}s") + +for name, shakes in [("Shake128: ", (Shake128, shake_128, shake_128_hashlib, SHAKE128_XOF())), + ("Shake256: ", (Shake256, shake_256, shake_256_hashlib, SHAKE256_XOF()))]: + print("="*80) + print(f" Benchmarking {name}") print("="*80) + for (c, n, number) in [(1, 10_000, 100), (100, 10_000, 100), (1000, 1000, 100), (10_000, 1000, 10), (32, 100_000, 10)]: + print(f"Requesting {c} bytes from XOF {n} times") + xoflib_time = timeit( + 'benchmark_xoflib_stream(shake, b"benchmarking...", c, n)', + globals={"shake" : shakes[0], "benchmark_xoflib_stream": benchmark_xoflib_stream, "c" : c, "n" : n}, + number = number + ) + print(f"xoflib: {xoflib_time:.2f}s") + + hashlib_single_time = timeit( + 'benchmark_hashlib_one_call(shake, b"benchmarking...", c, n)', + globals={"shake" : shakes[1], "benchmark_hashlib_one_call": benchmark_hashlib_one_call, "c" : c, "n" : n}, + number = number + ) + print(f"hashlib (single call): {hashlib_single_time:.2f}s") + + hashlib_stream_time = timeit( + 'benchmark_hashlib_stream(shake, b"benchmarking...", c, n)', + globals={"shake" : shakes[2], "benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, + number = number + ) + print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") + + pycryptodome_time = timeit( + 'benchmark_pycryptodome_stream(shake, b"benchmarking...", c, n)', + globals={"shake" : shakes[3], "benchmark_pycryptodome_stream": benchmark_pycryptodome_stream, "c" : c, "n" : n}, + number = number + ) + print(f"pycryptodome: {pycryptodome_time:.2f}s") + print("="*80) + -# print("-" * 80) - -# random.seed(0) -# t0 = time.time() -# xof = Shaker128(b"123").finalize() -# for _ in range(10_000): -# n = random.randint(1, 500) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 500)) with xoflib: {time.time() - t0 }") - -# random.seed(0) -# t0 = time.time() -# for _ in range(10_000): -# n = random.randint(1, 500) -# a = shake_128(b"123").digest(n) -# print(f"10_000 calls (read(1, 500)) with hashlib: {time.time() - t0 }") - -# random.seed(0) -# t0 = time.time() -# xof = SHAKE128_XOF() -# xof.update(b"123") -# for _ in range(10_000): -# n = random.randint(1, 500) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 500)) with pycryptodome: {time.time() - t0 }") - -# print("-" * 80) - -# t0 = time.time() -# xof = Shaker128(b"123").finalize() -# for _ in range(1_000_000): -# a = xof.read(1) -# print(f"1_000_000 single byte reads with xoflib: {time.time() - t0 }") - -# t0 = time.time() -# xof = SHAKE128_XOF() -# xof.update(b"123") -# for _ in range(1_000_000): -# a = xof.read(1) -# print(f"1_000_000 single byte reads pycryptodome: {time.time() - t0 }") - -# t0 = time.time() -# xof = Shaker128(b"123").finalize() -# for _ in range(1_000_000): -# a = xof.read(168) -# print(f"100_000 block reads with xoflib: {time.time() - t0 }") - -# t0 = time.time() -# xof = SHAKE128_XOF() -# xof.update(b"123") -# for _ in range(1_000_000): -# a = xof.read(168) -# print(f"100_000 block reads pycryptodome: {time.time() - t0 }") - -# print("-" * 80) - -# random.seed(0) -# t0 = time.time() -# xof = Shaker128(b"123").finalize() -# for _ in range(10_000): -# n = random.randint(1, 5000) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 5000)) with xoflib Shake128: {time.time() - t0 }") - -# random.seed(0) -# t0 = time.time() -# xof = Shaker256(b"123").finalize() -# for _ in range(10_000): -# n = random.randint(1, 5000) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 5000)) with xoflib Shaker256: {time.time() - t0 }") - -# random.seed(0) -# t0 = time.time() -# xof = TurboShaker128(1, b"123").finalize() -# for _ in range(10_000): -# n = random.randint(1, 5000) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker128: {time.time() - t0 }") - -# random.seed(0) -# t0 = time.time() -# xof = TurboShaker256(1, b"123").finalize() -# for _ in range(10_000): -# n = random.randint(1, 5000) -# a = xof.read(n) -# print(f"10_000 calls (read(1, 5000)) with xoflib TurboShaker256: {time.time() - t0 }") diff --git a/benchmarks/shake_wrapper.py b/benchmarks/shake_wrapper.py index 0ded839..f8232c1 100644 --- a/benchmarks/shake_wrapper.py +++ b/benchmarks/shake_wrapper.py @@ -1,4 +1,4 @@ -from hashlib import shake_128 +from hashlib import shake_128, shake_256 class ShakeStream: """ @@ -23,3 +23,6 @@ def read(self, n: int) -> bytes: def shake_128_hashlib(absorb): return ShakeStream(shake_128(absorb).digest) + +def shake_256_hashlib(absorb): + return ShakeStream(shake_256(absorb).digest) diff --git a/tests/test_xoflib.py b/tests/test_xoflib.py index a51c847..6ba03ce 100644 --- a/tests/test_xoflib.py +++ b/tests/test_xoflib.py @@ -1,5 +1,5 @@ from hashlib import shake_128, shake_256 -from xoflib import Shaker128, Shaker256 +from xoflib import Shake128, Shake256 import unittest @@ -18,9 +18,9 @@ def hashlib_test_many_calls(self, Shake, shake_hashlib): self.assertEqual(shake_hashlib(absorb_bytes).digest(l), output) def test_hashlib_shake128(self): - self.hashlib_test_long_calls(Shaker128, shake_128) - self.hashlib_test_many_calls(Shaker128, shake_128) + self.hashlib_test_long_calls(Shake128, shake_128) + self.hashlib_test_many_calls(Shake128, shake_128) def test_hashlib_shake256(self): - self.hashlib_test_long_calls(Shaker256, shake_256) - self.hashlib_test_many_calls(Shaker256, shake_256) + self.hashlib_test_long_calls(Shake256, shake_256) + self.hashlib_test_many_calls(Shake256, shake_256) diff --git a/xoflib.pyi b/xoflib.pyi index 4c68be3..a9ea990 100644 --- a/xoflib.pyi +++ b/xoflib.pyi @@ -1,4 +1,4 @@ -class Shaker128: +class Shake128: def __init__(self, input_bytes: bytes | None = None): ... @@ -12,7 +12,7 @@ class Sponge128: def read(self, n: int) -> bytes: ... -class Shaker256: +class Shake256: def __init__(self, input_bytes: bytes | None = None): ... From 670bb50abc057c41c4fe7928bcb0119610bcd79f Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 14:25:49 +0100 Subject: [PATCH 4/6] bench update --- benchmarks/benchmark_xof.py | 1 - 1 file changed, 1 deletion(-) diff --git a/benchmarks/benchmark_xof.py b/benchmarks/benchmark_xof.py index 1d80f1f..f3e6fa9 100644 --- a/benchmarks/benchmark_xof.py +++ b/benchmarks/benchmark_xof.py @@ -56,7 +56,6 @@ def benchmark_pycryptodome_stream(shake, absorb, c, n): b = benchmark_hashlib_one_call(shake_128, b"benchmarking...", 123, 1000) c = benchmark_hashlib_stream(shake_128_hashlib, b"benchmarking...", 123, 1000) d = benchmark_pycryptodome_stream(SHAKE128_XOF(), b"benchmarking...", 123, 1000) - assert a == b == c == d From d141bd51c1267284588b9cec32326677006cd2e5 Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 14:30:55 +0100 Subject: [PATCH 5/6] update readme --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e0f976c..b5aeceb 100644 --- a/README.md +++ b/README.md @@ -73,20 +73,59 @@ https://xoflib.readthedocs.io/ ## Rough Benchmark +We find that `xoflib` performs equally with `hashlib` and is faster than `pycryptodome`. + +`xoflib` has the additional memory cost benefit as calling `c` bytes to be read from our XOF `n` times only needs `c` bytes of memory for each call, where as `hashlib` requires the potentially colossal amount of `n * c` bytes of memory which are then iterated over. + +We include two timings for `hashlib` -- one naive where `n * c` bytes are requested and iterated over slicing over bytes and a second which uses a wrapper by David Buchanan +[from this comment](https://github.com/pyca/cryptography/issues/9185#issuecomment-1868518432) which helps with the API but has the same memory usage issues. + +All times are derived by timing the computation of `c_0 ^ c_1 ^ ... c_(n-1)` for `n` chunks of `c` bytes: + +```py +def benchmark_xof(shake, absorb, c, n): + xof = shake(absorb).finalize() + res = bytes([0] * c) + for _ in range(n): + chunk = xof.read(c) + res = xor_bytes(res, chunk) + return res +``` + ``` -10_000 calls (read(1, 500)) with xoflib: 0.014404773712158203 -10_000 calls (read(1, 500)) with hashlib: 0.02388787269592285 -10_000 calls (read(1, 500)) with pycryptodome: 0.028993844985961914 --------------------------------------------------------------------------------- -1_000_000 single byte reads with xoflib: 0.16383790969848633 -1_000_000 single byte reads pycryptodome: 1.172316312789917 -100_000 block reads with xoflib: 0.6025588512420654 -100_000 block reads pycryptodome: 1.6401760578155518 --------------------------------------------------------------------------------- -10_000 calls (read(1, 5000)) with xoflib Shake128: 0.07348895072937012 -10_000 calls (read(1, 5000)) with xoflib Shaker256: 0.08775138854980469 -10_000 calls (read(1, 5000)) with xoflib TurboShaker128: 0.04633498191833496 -10_000 calls (read(1, 5000)) with xoflib TurboShaker256: 0.056485891342163086 +================================================================================ +Benchmarking Shake128: +================================================================================ +Requesting 1 bytes from XOF 10000 times +xoflib: 0.71s +hashlib (single call): 0.66s +hashlib (streaming): 0.88s +pycryptodome: 2.12s +================================================================================ +Requesting 100 bytes from XOF 10000 times +xoflib: 8.67s +hashlib (single call): 7.69s +hashlib (streaming): 10.06s +pycryptodome: 11.15s +================================================================================ +Requesting 1000 bytes from XOF 1000 times +xoflib: 6.77s +hashlib (single call): 6.62s +hashlib (streaming): 7.22s +pycryptodome: 6.33s +================================================================================ +Requesting 10000 bytes from XOF 1000 times +xoflib: 6.32s +hashlib (single call): 6.37s +hashlib (streaming): 6.51s +pycryptodome: 6.45s +================================================================================ +Requesting 32 bytes from XOF 100000 times +xoflib: 2.80s +hashlib (single call): 2.69s +hashlib (streaming): 2.95s +pycryptodome: 4.04s +================================================================================ ``` For more information, see the file [`benchmarks/benchmark_xof.py`](benchmarks/benchmark_xof.py). From fdbd2f6636a16caeeac3808b544e79c92ba1bb54 Mon Sep 17 00:00:00 2001 From: Giacomo Pope Date: Mon, 29 Jul 2024 16:16:27 +0100 Subject: [PATCH 6/6] format and cleanup --- README.md | 51 ++++++++++---------- benchmarks/benchmark_xof.py | 94 ++++++++++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index f241710..c4ebd6b 100644 --- a/README.md +++ b/README.md @@ -102,38 +102,37 @@ def benchmark_xof(shake, absorb, c, n): ``` ================================================================================ -Benchmarking Shake128: + Benchmarking Shake256: ================================================================================ Requesting 1 bytes from XOF 10000 times -xoflib: 0.71s -hashlib (single call): 0.66s -hashlib (streaming): 0.88s -pycryptodome: 2.12s -================================================================================ +xoflib: 0.69s +hashlib (single call): 0.65s +hashlib (streaming): 0.82s +pycryptodome: 1.82s + Requesting 100 bytes from XOF 10000 times -xoflib: 8.67s -hashlib (single call): 7.69s -hashlib (streaming): 10.06s -pycryptodome: 11.15s -================================================================================ +xoflib: 6.65s +hashlib (single call): 6.57s +hashlib (streaming): 6.98s +pycryptodome: 7.83s + Requesting 1000 bytes from XOF 1000 times -xoflib: 6.77s -hashlib (single call): 6.62s -hashlib (streaming): 7.22s -pycryptodome: 6.33s -================================================================================ +xoflib: 6.05s +hashlib (single call): 5.90s +hashlib (streaming): 6.15s +pycryptodome: 6.15s + Requesting 10000 bytes from XOF 1000 times -xoflib: 6.32s -hashlib (single call): 6.37s -hashlib (streaming): 6.51s -pycryptodome: 6.45s -================================================================================ +xoflib: 5.82s +hashlib (single call): 5.77s +hashlib (streaming): 6.37s +pycryptodome: 5.85s + Requesting 32 bytes from XOF 100000 times -xoflib: 2.80s -hashlib (single call): 2.69s -hashlib (streaming): 2.95s -pycryptodome: 4.04s -================================================================================ +xoflib: 2.71s +hashlib (single call): 2.63s +hashlib (streaming): 2.89s +pycryptodome: 3.83s ``` For more information, see the file [`benchmarks/benchmark_xof.py`](benchmarks/benchmark_xof.py). diff --git a/benchmarks/benchmark_xof.py b/benchmarks/benchmark_xof.py index ec63d34..1f7c806 100644 --- a/benchmarks/benchmark_xof.py +++ b/benchmarks/benchmark_xof.py @@ -1,6 +1,6 @@ from timeit import timeit from hashlib import shake_128, shake_256 -from xoflib import Shake128, Shake256 +from xoflib import Shake128, Shake256, TurboShake128, TurboShake256 from Crypto.Hash.SHAKE128 import SHAKE128_XOF from Crypto.Hash.SHAKE256 import SHAKE256_XOF from shake_wrapper import shake_128_hashlib, shake_256_hashlib @@ -18,6 +18,16 @@ def benchmark_xoflib_stream(shake, absorb, c, n): res = xor_bytes(res, chunk) return res + +def benchmark_xoflib_turbo_stream(turboshake, absorb, c, n): + xof = turboshake(1, absorb).finalize() + res = bytes([0] * c) + for _ in range(n): + chunk = xof.read(c) + res = xor_bytes(res, chunk) + return res + + def benchmark_hashlib_one_call(shake, absorb, c, n): """ Requires generating all c * n bytes in one go @@ -31,6 +41,7 @@ def benchmark_hashlib_one_call(shake, absorb, c, n): res = xor_bytes(res, chunk) return res + def benchmark_hashlib_stream(shake, absorb, c, n): """ Requests only the bytes needed, but requires n calls to the digest @@ -42,6 +53,7 @@ def benchmark_hashlib_stream(shake, absorb, c, n): res = xor_bytes(res, chunk) return res + def benchmark_pycryptodome_stream(shake, absorb, c, n): shake.__init__() xof = shake.update(absorb) @@ -59,39 +71,87 @@ def benchmark_pycryptodome_stream(shake, absorb, c, n): d = benchmark_pycryptodome_stream(SHAKE128_XOF(), b"benchmarking...", 123, 1000) assert a == b == c == d +benchmark_data = [ + (1, 10_000, 100), + (100, 10_000, 100), + (1000, 1000, 100), + (10_000, 1000, 10), + (32, 100_000, 10), +] -for name, shakes in [("Shake128: ", (Shake128, shake_128, shake_128_hashlib, SHAKE128_XOF())), - ("Shake256: ", (Shake256, shake_256, shake_256_hashlib, SHAKE256_XOF()))]: - print("="*80) - print(f" Benchmarking {name}") - print("="*80) - for (c, n, number) in [(1, 10_000, 100), (100, 10_000, 100), (1000, 1000, 100), (10_000, 1000, 10), (32, 100_000, 10)]: +for name, shakes in [ + ("Shake128: ", (Shake128, shake_128, shake_128_hashlib, SHAKE128_XOF())), + ("Shake256: ", (Shake256, shake_256, shake_256_hashlib, SHAKE256_XOF())), +]: + print("=" * 80) + print(f"Benchmarking {name}") + print("=" * 80) + for c, n, number in benchmark_data: print(f"Requesting {c} bytes from XOF {n} times") xoflib_time = timeit( 'benchmark_xoflib_stream(shake, b"benchmarking...", c, n)', - globals={"shake" : shakes[0], "benchmark_xoflib_stream": benchmark_xoflib_stream, "c" : c, "n" : n}, - number = number + globals={ + "shake": shakes[0], + "benchmark_xoflib_stream": benchmark_xoflib_stream, + "c": c, + "n": n, + }, + number=number, ) print(f"xoflib: {xoflib_time:.2f}s") - + hashlib_single_time = timeit( 'benchmark_hashlib_one_call(shake, b"benchmarking...", c, n)', - globals={"shake" : shakes[1], "benchmark_hashlib_one_call": benchmark_hashlib_one_call, "c" : c, "n" : n}, - number = number + globals={ + "shake": shakes[1], + "benchmark_hashlib_one_call": benchmark_hashlib_one_call, + "c": c, + "n": n, + }, + number=number, ) print(f"hashlib (single call): {hashlib_single_time:.2f}s") hashlib_stream_time = timeit( 'benchmark_hashlib_stream(shake, b"benchmarking...", c, n)', - globals={"shake" : shakes[2], "benchmark_hashlib_stream": benchmark_hashlib_stream, "c" : c, "n" : n}, - number = number + globals={ + "shake": shakes[2], + "benchmark_hashlib_stream": benchmark_hashlib_stream, + "c": c, + "n": n, + }, + number=number, ) print(f"hashlib (streaming): {hashlib_stream_time:.2f}s") pycryptodome_time = timeit( 'benchmark_pycryptodome_stream(shake, b"benchmarking...", c, n)', - globals={"shake" : shakes[3], "benchmark_pycryptodome_stream": benchmark_pycryptodome_stream, "c" : c, "n" : n}, - number = number + globals={ + "shake": shakes[3], + "benchmark_pycryptodome_stream": benchmark_pycryptodome_stream, + "c": c, + "n": n, + }, + number=number, ) print(f"pycryptodome: {pycryptodome_time:.2f}s") - print("="*80) + print() + +for name, shake in [("TurboShake128", TurboShake128), ("TurboShake256", TurboShake256)]: + print("=" * 80) + print(f"Benchmarking {name}") + print("=" * 80) + for c, n, number in benchmark_data: + print(f"Requesting {c} bytes from XOF {n} times") + xoflib_time = timeit( + 'benchmark_xoflib_stream(shake, b"benchmarking...", c, n)', + globals={ + "shake": shakes[0], + "benchmark_xoflib_stream": benchmark_xoflib_stream, + "c": c, + "n": n, + }, + number=number, + ) + print(f"xoflib: {xoflib_time:.2f}s") + print()