# Hashing and Checksums

> StringZilla for Python

### One Shot Hashes and Checksums

- `sz.hash(text, seed=0)` — seeded 64-bit AES-accelerated hash, returned as an unsigned `int`.
  This differs from Python's built-in `hash()`, which returns a platform-dependent `Py_hash_t`.
- `sz.hash_multiseed(text, seeds, out=None)` — hash one string under many seeds at once.
  `seeds` is a contiguous buffer of `uint64`, such as a `numpy.uint64` array or `array('Q', ...)`; plain `int` lists are not accepted.
  Returns a tuple of ints, or fills the optional contiguous `uint64` `out` buffer in place and returns `None`.
  This is much faster than looping `hash` for short strings under many seeds, and is useful for feature hashing, Count-Min sketches, Bloom/cuckoo filters, and MinHash/LSH.
- `sz.bytesum(text)` — additive checksum of the individual byte values, as an `int`.
- `sz.sha256(text)` — 32-byte SHA-256 digest as `bytes`.
- `sz.hmac_sha256(key, message)` — 32-byte HMAC-SHA256 digest as `bytes`.

`hash`, `hash_multiseed`, `bytesum`, and `sha256` are also available as `String` methods.

```python
import stringzilla as sz
import numpy as np

sz.hash("hello")                  # seeded 64-bit value
sz.hash("hello", seed=42)         # a different value
assert sz.bytesum("abc") == 294
assert sz.sha256("abc").hex()[:8] == "ba7816bf"
assert len(sz.hmac_sha256(b"key", b"message")) == 32

seeds = np.arange(8, dtype=np.uint64)
assert len(sz.hash_multiseed("hello", seeds)) == 8   # tuple of 8 ints
```

### Incremental 64-bit `Hasher`

`Hasher(seed=0)` is an incremental, AES-accelerated hasher producing the same 64-bit value as `sz.hash`.

- `update(data)` — absorb a `str`/`bytes` chunk; returns `self` for chaining.
- `digest()` — current hash as an unsigned 64-bit `int`, without consuming state.
- `hexdigest()` — current hash as a 16-character lowercase hex `str`.
- `reset()` — reset to the initial seed; returns `self`.

```python
import stringzilla as sz

h = sz.Hasher()
h.update(b"hello").update(b" world")
assert h.digest() == sz.hash(b"hello world")
assert len(h.hexdigest()) == 16
```

### Incremental `Sha256` Compatible with `hashlib`

`Sha256()` is an incremental SHA-256 hasher with a `hashlib`-compatible interface, hardware-accelerated where available.

- `update(data)` — absorb a `str`/`bytes` chunk; returns `self`.
- `digest()` — current 32-byte digest as `bytes`.
- `hexdigest()` — current digest as a 64-character lowercase hex `str`.
- `reset()` — reset to the initial SHA-256 constants; returns `self`.
- `copy()` — return an independent copy with identical internal state.

```python
import stringzilla as sz
import hashlib

assert sz.Sha256().update(b"abc").hexdigest() == hashlib.sha256(b"abc").hexdigest()

a = sz.Sha256().update(b"ab")
assert a.copy().update(b"c").hexdigest() == sz.Sha256().update(b"abc").hexdigest()
```

### `Sha256s` for Many Messages at Once

Digesting one message is a serial dependency chain, but independent messages compress in parallel lanes — sixteen at a time on AVX-512, eight on AVX2.
`Sha256s(lanes)` holds one hasher per lane, with `update(chunks)` taking exactly one chunk per lane and `digest(out=None)`, `hexdigest()`, `reset()`, `copy()` and `len()` behaving as on `Sha256`.
Lanes are workers rather than messages: `lanes[i]` reads one as a `Sha256`, and `lanes[i] = sz.Sha256()` retires a finished lane while the others keep streaming.

`sz.hmac_sha256(key, message, out=None)` takes either one message or a collection, authenticating a batch of tokens sharing a secret through the same kernels.

Both accept a `Strings`, read in place, and an `out` buffer of bytes — a `(count, 32)` `numpy.uint8` matrix receives one digest per row with no allocation, since that is byte-for-byte the layout the kernel writes.
The GIL is released around the kernel, so several threads digest concurrently.

Lanes advance in lockstep within a group, so a group costs as much as its longest message.
Every length is handled correctly, but throughput is best when messages of similar length share a group, which `Strings.argsort` arranges.

```python
import numpy as np
import stringzilla as sz

texts = sz.Strings(["alpha", "beta", "gamma"])
lanes = sz.Sha256s(len(texts))
digests = np.empty((len(lanes), sz.Sha256.digest_length), np.uint8)
lanes.update(texts).digest(out=digests)

tags = sz.hmac_sha256(b"secret", texts)   # one 32-byte tag per message
assert tags[0] == sz.hmac_sha256(b"secret", "alpha")
```

`import stringzilla`

`from stringzilla import Aes256CtrKey, Aes256GcmKey, Aes256GcmEncryptor, …`

## AuthenticationError

Raised when a Galois/counter mode tag does not authenticate the bytes it accompanies.

```python
class AuthenticationError(ValueError)
```

Derives from `ValueError`, so code guarding only against malformed input still stops a
forgery, and the plaintext is never returned alongside it.

Examples: ```
>>> issubclass(sz.AuthenticationError, ValueError)
True
```

## Aes256CtrKey

AES-256 in counter mode, abbreviated CTR: unauthenticated, seekable, and its own inverse.

```python
class Aes256CtrKey
```

The 32-byte secret is expanded once into a round-key schedule the object holds and wipes when it is collected.

Raises:
ValueError: If the secret is not exactly 32 bytes long.
Note:
A nonce must never repeat under one secret; reuse exposes the exclusive-or of two texts.

Examples: ```
>>> key = sz.Aes256CtrKey(bytes(32))
>>> key.xor(key.xor(b'hello', bytes(12)), bytes(12))
b'hello'
```

## Aes256CtrKey

```python
Aes256CtrKey(
    secret: str | bytes,
    *,
    capabilities: Capability | None = None,
)
```

## xor

xor(text, nonce, offset=0) -> bytes

```python
def xor(
    text: str | bytes,
    nonce: str | bytes,
    offset: int = 0,
) -> bytes
```

Exclusive-or `text` against the keystream, encrypting or decrypting with the same call.

Returns:
bytes: As many bytes as `text` holds.
Raises:
ValueError: If the nonce is not exactly 12 bytes long.

- `text`: The bytes to transform.
- `nonce`: Exactly 12 nonce bytes, never repeated under one secret.
- `offset`: Absolute byte position of `text` in the stream. Defaults to 0.

Examples: ```
>>> key, nonce = sz.Aes256CtrKey(bytes(32)), bytes(12)
>>> whole = key.xor(bytes(64), nonce)
>>> key.xor(bytes(16), nonce, offset=32) == whole[32:48]
True
```

## Aes256GcmKey

AES-256 in Galois/counter mode, abbreviated GCM: authenticated, and therefore not seekable.

```python
class Aes256GcmKey
```

The 32-byte secret is expanded once into a schedule and the Galois hash powers, also called GHASH, and is wiped on collection.

Raises:
ValueError: If the secret is not exactly 32 bytes long.
Note:
Reusing a nonce here exposes the hash subkey, after which every message can be forged.

Examples: ```
>>> key = sz.Aes256GcmKey(bytes(32))
>>> ciphertext, tag = key.encrypt(b'hello', bytes(12))
>>> key.decrypt(ciphertext, bytes(12), tag)
b'hello'
```

## Aes256GcmKey

```python
Aes256GcmKey(
    secret: str | bytes,
    *,
    capabilities: Capability | None = None,
)
```

## encrypt

Encrypt `text` and authenticate both it and `associated`, returning ciphertext and tag.

```python
def encrypt(
    text: str | bytes,
    nonce: str | bytes,
    associated: str | bytes | None = None,
) -> tuple[bytes, bytes]
```

Returns:
tuple: The ciphertext, as long as `text`, and the 16-byte tag.
Raises:
ValueError: If the nonce is not exactly 12 bytes long.

- `text`: The plaintext.
- `nonce`: Exactly 12 nonce bytes, never repeated under one secret.
- `associated`: Bytes authenticated but not encrypted.

Examples: ```
>>> ciphertext, tag = sz.Aes256GcmKey(bytes(32)).encrypt(b'hi', bytes(12), b'header')
>>> len(ciphertext), len(tag)
(2, 16)
```

## decrypt

decrypt(text, nonce, tag, associated=None) -> bytes

```python
def decrypt(
    text: str | bytes,
    nonce: str | bytes,
    tag: str | bytes,
    associated: str | bytes | None = None,
) -> bytes
```

Check `tag` against the ciphertext and associated data, then return the plaintext.

Returns:
bytes: The plaintext, as long as `text`.
Raises:
AuthenticationError: If the tag does not match, in which case nothing is returned.

- `text`: The ciphertext.
- `nonce`: The 12 nonce bytes used to encrypt.
- `tag`: The 16-byte tag to check.
- `associated`: The same bytes passed to [`encrypt`](#encrypt).

Examples: ```
>>> key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
>>> ciphertext, tag = key.encrypt(b'hello', nonce)
>>> key.decrypt(ciphertext, nonce, tag)
b'hello'
```

## Aes256GcmEncryptor

The same Galois/counter mode construction as [`Aes256GcmKey`](#Aes256GcmKey), sealing a message in chunks.

```python
class Aes256GcmEncryptor
```

Chunk boundaries are invisible to the result. Opening a message uses [`Aes256GcmDecryptor`](#Aes256GcmDecryptor), a separate type because the tag absorbs ciphertext in both directions, which is the output when sealing and the input when opening. Copies the key, so it cannot outlive it.

Examples: ```
>>> key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
>>> sealed = sz.Aes256GcmEncryptor(key, nonce)
>>> ciphertext, tag = key.encrypt(b'hello', nonce)
>>> sealed.encrypt(b'hel') + sealed.encrypt(b'lo') == ciphertext
True
```

## Aes256GcmEncryptor

```python
Aes256GcmEncryptor(
    key: Aes256GcmKey,
    nonce: str | bytes,
    *,
    capabilities: Capability | None = None,
)
```

## associate

associate(data) -> Aes256GcmEncryptor

```python
def associate(data: str | bytes, /) -> Aes256GcmEncryptor
```

Absorb bytes that are authenticated but not encrypted, and return self for chaining.
All associated data must be absorbed before the first message chunk.

Returns:
Aes256GcmEncryptor: The same object, enabling `sealed.associate(a).associate(b)`.
Example:

key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
sealed = sz.Aes256GcmEncryptor(key, nonce).associate(b'head')
ciphertext, tag = key.encrypt(b'body', nonce, b'head')
sealed.encrypt(b'body') == ciphertext
True

- `data`: Associated bytes, such as a routing header.

## encrypt

encrypt(data) -> bytes

```python
def encrypt(data: str | bytes, /) -> bytes
```

Encrypt one chunk and absorb its ciphertext into the running tag.

Returns:
bytes: The ciphertext chunk, as long as `data`.
Example:

sealed = sz.Aes256GcmEncryptor(sz.Aes256GcmKey(bytes(32)), bytes(12))
len(sealed.encrypt(b'chunk'))
5

- `data`: The plaintext chunk.

## *

digest() -> bytes

```python
def digest() -> bytes
```

Return the 16-byte tag over everything absorbed so far, leaving the seal appendable.

Example:

key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
sealed = sz.Aes256GcmEncryptor(key, nonce)
_ = sealed.encrypt(b'hello')
ciphertext, tag = key.encrypt(b'hello', nonce)
sealed.digest() == tag
True

Return the current 32-byte SHA-256 digest without consuming state.

Example:

len(sz.Sha256().update(b'abc').digest())
32

## Aes256GcmDecryptor

The same Galois/counter mode construction as [`Aes256GcmKey`](#Aes256GcmKey), opening a message in chunks and checking its tag at the end.

```python
class Aes256GcmDecryptor
```

Chunk boundaries are invisible to the result. Sealing a message uses [`Aes256GcmEncryptor`](#Aes256GcmEncryptor). Copies the key, so it cannot outlive it.

Examples: ```
>>> key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
>>> ciphertext, tag = key.encrypt(b'hello', nonce)
>>> sz.Aes256GcmDecryptor(key, nonce).decrypt_unverified(ciphertext)
b'hello'
```

## Aes256GcmDecryptor

```python
Aes256GcmDecryptor(
    key: Aes256GcmKey,
    nonce: str | bytes,
    *,
    capabilities: Capability | None = None,
)
```

## associate

associate(data) -> Aes256GcmDecryptor

```python
def associate(data: str | bytes, /) -> Aes256GcmDecryptor
```

Absorb bytes that are authenticated but not encrypted, and return self for chaining.
All associated data must be absorbed before the first message chunk.

Returns:
Aes256GcmDecryptor: The same object, enabling `opened.associate(a).associate(b)`.
Example:

key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
ciphertext, tag = key.encrypt(b'body', nonce, b'head')
opened = sz.Aes256GcmDecryptor(key, nonce).associate(b'head')
opened.decrypt_unverified(ciphertext)
b'body'

- `data`: The same associated bytes the sender authenticated.

## decrypt_unverified

decrypt_unverified(data) -> bytes

```python
def decrypt_unverified(data: str | bytes, /) -> bytes
```

Decrypt one chunk and absorb its ciphertext into the running tag. The bytes returned are
not yet authenticated: nothing has checked that this ciphertext is genuine, so buffer them
until `verify()` succeeds rather than acting on them.

Returns:
bytes: The unverified plaintext chunk, as long as `data`.
Example:

key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
ciphertext, tag = key.encrypt(b'hello', nonce)
opened = sz.Aes256GcmDecryptor(key, nonce)
opened.decrypt_unverified(ciphertext)
b'hello'

- `data`: The ciphertext chunk.

## verify

verify(tag) -> None

```python
def verify(tag: str | bytes, /) -> None
```

Check `tag` against everything absorbed so far, in time independent of where it differs.

Raises:
AuthenticationError: If the tag does not match.
ValueError: If the tag is not exactly 16 bytes long.
Example:

key, nonce = sz.Aes256GcmKey(bytes(32)), bytes(12)
ciphertext, tag = key.encrypt(b'hello', nonce)
opened = sz.Aes256GcmDecryptor(key, nonce)
_ = opened.decrypt_unverified(ciphertext)
opened.verify(tag) is None
True

- `tag`: The 16-byte tag to check.

## Hasher

Incremental, AES-accelerated hasher producing the same 64-bit value as [`sz.hash`](#hash).

```python
class Hasher
```

Feed data in chunks with `update()`, read the digest, and reuse the object via `reset()`.

Example:

h = sz.Hasher()
_ = h.update(b'hello').update(b' world')
h.digest() == sz.hash(b'hello world')
True

## Hasher

```python
Hasher(seed: int = 0, *, capabilities: Capability | None = None)
```

## update

update(data) -> Hasher

```python
def update(data: str | bytes, /) -> Hasher
```

Absorb more bytes into the running hash and return self for chaining.

Returns:
Hasher: The same object, enabling `h.update(a).update(b)`.
Example:

sz.Hasher().update(b'ab').update(b'c').digest() == sz.hash(b'abc')
True

- `data`: Chunk to mix into the state.

## digest

digest() -> int

```python
def digest() -> int
```

Return the current hash as an unsigned 64-bit integer without consuming state.

Example:

isinstance(sz.Hasher().update(b'x').digest(), int)
True

## *

hexdigest() -> str

```python
def hexdigest() -> str
```

Return the current hash as a 16-character lowercase hex string.

Example:

len(sz.Hasher().hexdigest())
16

Return the current SHA-256 digest as a 64-character lowercase hex string.

Example:

import hashlib
sz.Sha256().update(b'abc').hexdigest() == hashlib.sha256(b'abc').hexdigest()
True

## reset

reset() -> Hasher

```python
def reset() -> Hasher
```

Reset the state to the initial seed and return self.

Example:

h = sz.Hasher().update(b'data').reset()
h.digest() == sz.Hasher().digest()
True

## Sha256

Incremental SHA-256 hasher with a hashlib-compatible interface, hardware-accelerated where available.

```python
class Sha256
```

Feed data with `update()`, then read `digest()` or `hexdigest()`. The capabilities it and its calls run default to the CPU's enabled ones.

Examples: ```
>>> import hashlib
>>> sz.Sha256().update(b'abc').hexdigest() == hashlib.sha256(b'abc').hexdigest()
True
```

## Sha256

```python
Sha256(*, capabilities: Capability | None = None)
```

## update

update(data) -> Sha256

```python
def update(data: str | bytes, /) -> Sha256
```

Absorb more bytes into the running SHA-256 state and return self for chaining.

Returns:
Sha256: The same object, enabling `h.update(a).update(b)`.
Example:

hexdigest = sz.Sha256().update(b'a').update(b'bc').hexdigest()
hexdigest[:8]
'ba7816bf'

- `data`: Chunk to mix into the state.

## *

reset() -> Sha256

```python
def reset() -> Sha256
```

- `reset`
- `copy`

Reset the state to the initial SHA-256 constants and return self.

Example:

h = sz.Sha256().update(b'x').reset()
h.hexdigest() == sz.Sha256().hexdigest()
True

Return an independent copy of the hasher with identical internal state.

Example:

a = sz.Sha256().update(b'ab')
a.copy().update(b'c').hexdigest() == sz.Sha256().update(b'abc').hexdigest()
True

## digest_length

```python
digest_length: int
```

## Sha256s

Incremental SHA-256 over many independent messages at once, one message per lane.

```python
class Sha256s
```

Hashing a single message is a serial dependency chain no instruction set can speed
up, but independent messages compress in parallel lanes: sixteen at a time on
AVX-512, eight on AVX2. Feed at least a few kilobytes per lane per call, as shorter
chunks never reach the lane-parallel path.

Lanes are workers rather than messages: assigning `lanes[i] = sz.Sha256()` retires a
finished lane and starts the next message in it, leaving every other lane streaming.

Example:

lanes = sz.Sha256s(2)
_ = lanes.update([b'Hello, ', b'Goodbye, '])
_ = lanes.update([b'world!', b'world!'])
hello, goodbye = lanes.digest()
hello == sz.Sha256().update(b'Hello, world!').digest()
True

## Sha256s

```python
Sha256s(lanes: int, /, *, capabilities: Capability | None = None)
```

## update

update(chunks) -> Sha256s

```python
def update(
    chunks: Strings | Sequence[str | bytes],
    /,
) -> Sha256s
```

Append the next chunk of every lane's message.

Returns:
Sha256s: The same object, to allow chaining.

- `chunks`: Exactly one string-like chunk per lane; may be empty. A Strings
is read in place, whatever its layout, so a tape never leaves its buffer.

## digest

digest(out=None) -> list[bytes] | buffer

```python
def digest(out: _Buffer | None = None) -> list[bytes] | _Buffer
```

Return each lane's 32-byte digest, leaving every lane able to accept more data.

Returns:
list[bytes]: One digest per lane in lane order, or `out` itself when a buffer is given.
Example:

lanes = sz.Sha256s(2)
digests = bytearray(len(lanes) * sz.Sha256.digest_length)
_ = lanes.update([b'abc', b'abc']).digest(out=digests)
bytes(digests[:sz.Sha256.digest_length]) == sz.Sha256().update(b'abc').digest()
True

- `out`: Writable, C-contiguous buffer of bytes holding at least
`len(lanes) * 32` of them - a `(len(lanes), 32)` numpy.uint8 matrix receives one
digest per row with zero allocation. Defaults to None.

## hexdigest

hexdigest() -> list[str]

```python
def hexdigest() -> list[str]
```

Return each lane's digest as a 64-character lowercase hex string, in lane order.

## *

reset() -> Sha256s

```python
def reset() -> Sha256s
```

- `reset`
- `copy`

Return every lane to the initial state, reusing the allocation.

Return an independent batch whose lanes hold identical internal state.

## digest_length

```python
digest_length: int
```

## hash

Compute the hash value of the string.

```python
def hash(
    text: _StringLike,
    seed: int = 0,
    /,
    *,
    capabilities: Capability | None = None,
) -> int
```

This function can be called as a method on a String object or as a standalone function.
Args:
text (String or str or bytes): The string to hash, positional-only when standalone.
seed (int, optional): The seed value for hashing. Defaults to 0. Can be positional or keyword.
capabilities (Capability, optional): Capabilities to run, defaulting to the CPU's enabled ones.
Returns:
int: The hash value as an unsigned 64-bit integer. This differs from Python's
built-in `hash()` which returns a `Py_hash_t` and may be platform-dependent.
Raises:
TypeError: If the argument is not string-like or incorrect number of arguments is provided.
Signature:

def hash(text, seed=0, /, *, capabilities=None) -> int: ...

## hash_multiseed

Hash one string under many seeds at once.

```python
def hash_multiseed(
    text: _StringLike,
    seeds: _Buffer,
    /,
    out: _Buffer | None = None,
    *,
    capabilities: Capability | None = None,
) -> tuple[int, ...] | None
```

Equivalent to `tuple(hash(text, s) for s in seeds)`, but normalizes the input into AES
blocks once and replays the cheap per-seed rounds - markedly faster for short strings under
many seeds (feature hashing, Count-Min sketches, Bloom/cuckoo filters, MinHash/LSH).

Returns:
tuple[int, ...] | None: Tuple of 64-bit hashes, or None when writing into `out`.

- `text`: The string to hash, positional-only when standalone.
- `seeds`: Contiguous 64-bit seeds, e.g. numpy.uint64 array or array('Q', ...).
Plain int lists are not accepted - wrap them in array('Q', seeds) or numpy.
- `out`: A contiguous buffer of at least len(seeds) elements.
If given, it is filled in place and None returned; otherwise a tuple of ints is returned.
- `capabilities`: Capabilities to run, defaulting to the CPU's enabled ones.

Examples: ```
>>> def hash_multiseed(text, seeds, /, out=None, *, capabilities=None) -> tuple | None: ...
```

## bytesum

Compute the checksum of individual byte values in a string.

```python
def bytesum(
    text: _StringLike,
    *,
    capabilities: Capability | None = None,
) -> int
```

This function can be called as a method on a String object or as a standalone function.
Args:
text (String or str or bytes): The string to hash.
capabilities (Capability, optional): Capabilities to run, defaulting to the CPU's enabled ones.
Returns:
int: The checksum of individual byte values in a string.
Raises:
TypeError: If the argument is not string-like or incorrect number of arguments is provided.

Examples: ```
>>> sz.bytesum('abc')
294
```

## sha256

Compute SHA256 cryptographic hash of the input data.

```python
def sha256(
    text: _StringLike,
    *,
    capabilities: Capability | None = None,
) -> bytes
```

This function can be called as a method on a String object or as a standalone function.
Args:
text (String or str or bytes): The input data to hash.
capabilities (Capability, optional): Capabilities to run, defaulting to the CPU's enabled ones.
Returns:
bytes: The 32-byte (256-bit) SHA256 digest.
Raises:
TypeError: If the argument is not string-like or incorrect number of arguments is provided.

Examples: ```
>>> digest = sz.String('abc').sha256().hex()
>>> digest[:8]
'ba7816bf'
```

## hmac_sha256

hmac_sha256(key, message, out=None, *, capabilities=None) -> bytes | list[bytes] | buffer

```python
def hmac_sha256(
    key: str | bytes,
    message: str | bytes | Strings | Sequence[str | bytes],
    out: _Buffer | None = None,
    *,
    capabilities: Capability | None = None,
) -> bytes | list[bytes] | _Buffer
```

Compute the HMAC-SHA256 authentication code of one message, or of many under one key.

Authenticating one message is a serial dependency chain no instruction set can speed up, but
independent messages compress in parallel lanes: sixteen at a time on AVX-512, eight on
AVX2. Pass a Strings of messages - a batch of tokens or requests sharing a secret - and both
the message pass and the outer wrap run lane-parallel.

Returns:
bytes: The 32-byte tag, for a single string-like message.
list[bytes]: One tag per message, in order, for a collection.
buffer: `out` itself, when an output buffer is given.
Example:

len(sz.hmac_sha256(b'key', b'message'))
32
sz.hmac_sha256(b'key', sz.Strings(['a', 'b'])) == [
...     sz.hmac_sha256(b'key', b'a'), sz.hmac_sha256(b'key', b'b')]
True

- `key`: The secret key.
- `message`: One message, or many to authenticate together.
- `out`: Writable, C-contiguous buffer of bytes holding at least 32 per
message - a `(len(message), 32)` numpy.uint8 matrix receives one tag per row with
zero allocation. Defaults to None.
- `capabilities`: Capabilities to run, defaulting to the CPU's enabled ones.

