provsum 0.1.0

Fixed-size provenance digests for computation equivalence
Documentation
  • Coverage
  • 71.05%
    27 out of 38 items documented0 out of 27 items with examples
  • Size
  • Source code size: 33.4 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 551.1 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • rescrv/blue
    53 4 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • rescrv

provsum

Fixed-size digests of error provenance, for checking that a Rust port raises the same errors for the same reasons as the code it replaces.

Construction

Every error-origin, wrap, and pick site in the source gets a 16-byte GUID, carried verbatim across both implementations. Each GUID is hashed to a nonzero point in F_p (p = 2^61−1) per lane (two lanes). The digest of an error is its N[X] provenance polynomial evaluated at those points:

Site is the GUID's 16 bytes in canonical order, not a native-endian integer. Point derivation reads its two consecutive 8-byte chunks as little-endian, and digest serialization is little-endian too, so neither operation depends on the host architecture. A byte-array literal therefore produces the same result on every supported platform. When starting from textual UUID syntax, use the parser's canonical/RFC byte sequence in every language; in particular, do not mix a Windows GUID's field-wise little-endian representation with RFC bytes.

operation polynomial digest
Digest::origin(site) x_site r_site
d.wrap(site) x_site · d r_site · d
a.merge(b) a + b a + b
Digest::pick(site, [d0,d1..]) Σ x_{site,i} · d_i Σ r_{site,i} · d_i

pick records an ordered choice: slot 0 is the error that was returned, later slots were considered and discarded. Order enters the commutative semiring through per-slot variables, not through the operators, so swapping which error wins changes the polynomial. Plain wrap order is not recorded (x·y = y·x); that is intentional — a port that restructures control flow but preserves which sites contribute and how deeply should digest identically.

Digests are 18 bytes: two 8-byte lanes plus a 2-byte depth bound. Equality compares lanes only. Two digests of distinct polynomials of depth ≤ d collide with probability ≤ (d/(p−1))² ≈ 2^(2·log2 d − 122). This is a non-adversarial construction: it assumes nobody is choosing GUIDs or points to force a collision. Digest::log2_collision_bound() reports the bound for a given depth.

Diagnostics

provsum::poly::Poly is the expanded polynomial with the same API. Poly::digest() evaluates it at the same points, so poly.digest() == streaming_digest is a testable invariant, and Poly::diff names the monomials that differ between two implementations. Keep it behind a debug flag; its size is the number of distinct derivations, which is fine at a few thousand sites and shallow depth and unbounded in general.

Cross-language

python/provsum.py is a reference implementation. cargo run --example vectors and python3 python/provsum.py must print identical output; tests/vectors.rs pins those values. Point derivation is a splitmix64 finalizer over (seed, lane, slot) then the two GUID halves, rejection-sampled to be nonzero mod p — deliberately dependency-free so it is trivial to mirror.

Usage sketch

use provsum::{Digest, Site};

const PARSE: Site = Site(*b"\x01\x02..............");   // real GUIDs in practice
const READ:  Site = Site(*b"\x03\x04..............");
const RETRY: Site = Site(*b"\x05\x06..............");

struct Error { msg: String, prov: Digest }

fn read(path: &str) -> Result<Vec<u8>, Error> {
    std::fs::read(path).map_err(|e| Error { msg: e.to_string(), prov: Digest::origin(&READ) })
}

fn parse(path: &str) -> Result<Config, Error> {
    let bytes = read(path).map_err(|e| Error { prov: e.prov.wrap(&PARSE), ..e })?;
    // ...
}

fn with_fallback(primary: &str, fallback: &str) -> Result<Config, Error> {
    match (parse(primary), parse(fallback)) {
        (Ok(c), _) | (_, Ok(c)) => Ok(c),
        (Err(a), Err(b)) => Err(Error { prov: Digest::pick2(&RETRY, &a.prov, &b.prov), ..a }),
    }
}

The Python side does the same with Digest.origin(READ).wrap(PARSE) etc.; the differential harness runs both on the same inputs and compares prov.to_bytes().