bankml / docs /modules /sha256.md
Gregory-L's picture
bankML: the whole source (github.com/cryptoAGI/bankml @ 12ae409) and its page, with the bankML persona; the live engine (Dockerfile, hf/start.sh) ready for Docker hardware
28c70af verified
|
Raw History Blame Contribute Delete
5.35 kB
# `bankML/sha256.rs` — SHA-256 for the model pin, and the FORK.json pin scanner
## Summary
`sha256.rs` is a dependency-free SHA-256 (FIPS 180-4) and the scanner that finds a file's pinned hash in a
`FORK.json`. A GGUF plays only if its sha256 equals the record in the PYTHAI fork's `FORK.json`
(`files[].sha256` for its path).
On x86_64 it uses the CPU's SHA extensions (SHA-NI: AMD Zen, Intel Ice Lake and later) when present and the
portable rounds otherwise. Both give the same digest.
Callers: `bankml::pin` and `bankml::verify` (the second half of the gate, see [bankml.md](bankml.md)), so every
`bankml serve` start and every model switch hashes the whole model. Also `bankml sha256 FILE`; the receipt's
`request_sha256` and `response_sha256` (`serve::Tally`); since 0.3.8 the checksum that closes every slot file
(`Native::save_slot` / `restore_slot`); and the kernel oracles, which hash dequantized tensors to compare them with
ggml's.
## Technical usage
```rust
pub struct Sha256 { /* private */ } // impl Default
impl Sha256 {
pub fn update(&mut self, data: &[u8])
pub fn finish(self) -> [u8; 32]
}
pub fn hex(b: &[u8]) -> String
pub fn file_hex(p: &std::path::Path) -> std::io::Result<String>
pub fn pinned_sha256(fork_json: &str, name: &str) -> Option<String>
```
- `Sha256::default()` starts a hash; `update` streams any number of bytes; `finish` pads and returns the digest.
- `file_hex` hashes a file in 1 MiB reads and returns lowercase hex.
- `pinned_sha256` returns the lowercase 64-hex `"sha256"` of the object in `"files"` whose `"path"` equals `name`.
It returns `None` (unpinned, refuse) when there is no such object, or when its sha256 is missing or not 64 hex
digits. Key order inside the object does not matter.
Environment:
| variable | effect |
|---|---|
| `BANKML_NO_SHANI` | set: hash with the portable rounds instead of SHA-NI (read once, then cached) |
```sh
bankml sha256 .models/Bonsai-8B-Q1_0.gguf # prints "<hex> <file>", like sha256sum
bankml pin .models/Bonsai-8B-Q1_0.gguf --fork FORK.json
```
```rust
let mut h = bankml::sha256::Sha256::default();
h.update(b"abc");
assert_eq!(bankml::sha256::hex(&h.finish()), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
```
## How it is verified
- `fips_vectors`: the FIPS 180-4 vectors for `""`, `"abc"`, the 448-bit message, and one million `'a'` streamed in
1,000-byte updates across block boundaries.
- `hardware_path_equals_portable_on_every_length`: the dispatching path (SHA-NI when the CPU has it) against the
portable rounds on every length 0–1,000, on 4,095 / 4,096 / 4,097 / 65,536 / 69,999 bytes, and on split updates.
The test prints which hardware path it exercised.
- `fork_json_pin`: lowercase normalisation, a non-64-hex record treated as unpinned, a missing file, `sha256` before
`path`, and the compact shape `fork_bonsai.py` writes.
- Per [oracles.md](../oracles.md) §4, a real 1.16 GB model must hash to coreutils `sha256sum`'s value and its
published pin. The Rust pin was also checked against the real PYTHAI/Bonsai-8B-gguf-fork `FORK.json`.
- `testing/cli.rs`: `pin_and_verify`.
## Advantages and efficiency
- **No crate for the trust root.** The hash that decides whether a model may answer is one file in the crate, with
published test vectors as its oracle.
- **Hardware rounds, chosen at run time.** The SHA-NI path does two rounds per `sha256rnds2` and the message
schedule with `sha256msg1`/`sha256msg2`, following Intel's reference layout. Detection
(`is_x86_feature_detected!` for sha, sse4.1, ssse3) runs once and is cached in an atomic. The release build needs no
`target-cpu` flag.
- **Measured** (laptop, Ryzen 3 3200U, [PERFORMANCE.md](../PERFORMANCE.md), CHANGELOG 0.1.8): 0.23 s against 1.25 s
portable for a 248 MB model (5.5×); 2.9 s for the 1.16 GB 8B model; restarting the 8B carrier (hash and load) went
from 25 s to 7.0 s. Cold against warm hashing of the 1.7B model measured 364 against 419 MB/s; the disk keeps up
with SHA-NI, so no read-ahead hint was adopted.
- **Streaming, bounded memory.** `file_hex` uses one 1 MiB buffer whatever the file size. `update` hands whole
64-byte blocks to the compressor in one call and keeps only a partial block between calls.
- **Narrow scanner, not a JSON parser.** `pinned_sha256` looks for exactly one shape: anything it cannot read as
a 64-hex record for that path is `None`, which refuses.
- **Practice.** `unsafe` is confined to the `#[target_feature(enable = "sha,sse2,ssse3,sse4.1")]` intrinsic function,
called only after detection, with the safety condition stated at the call.
## Limitations
- The scanner is deliberately narrow. It expects the path and sha256 as plain JSON strings (the comment notes no
escapes are needed); it is not a general JSON parser.
- SHA-NI is used only on x86_64. Other targets run the portable rounds.
- Hashing reads the whole file, so the pin costs time proportional to model size on every verification. The guard
runs first so a bad header is refused before this cost.
## See also
- [bankml.md](bankml.md) (`pin`, `verify`), [gguf.md](gguf.md) (the guard that runs first)
- [../oracles.md](../oracles.md) §4, [../PERFORMANCE.md](../PERFORMANCE.md) (0.1.8 – 0.2.0 table)
- [../usage.md](../usage.md) §9 (receipts), §13 (environment)