a11oy / kipu_qillqaq /coding.py
betterwithage's picture
sync(space): full source mirror — resolve all GitHub<->Space drift (CTO)
a6a5d8e verified
Raw
History Blame
6.87 kB
"""Reed-Solomon erasure coding for KIPU receipt-cell durability.
HONEST NAMING: this is Reed-Solomon (Reed & Solomon, 1960) — the same MDS erasure code
used by RAID-6, CD/DVD/Blu-ray, QR codes, and Backblaze's storage pods. It is NOT
"holographic quantum error correction". An (n, k) RS code splits data into k data shards
plus (n-k) parity shards; ANY k of the n shards reconstruct the original. We default to
RS(10, 6): 6 data + 4 parity, surviving loss of any 4 of 10 shards (40% loss tolerance).
Implementation strategy (open-source only, graceful):
* If the `reedsolo` package is installed, use it (battle-tested GF(2^8) RS).
* Otherwise use a self-contained pure-Python RS over GF(2^8) implemented here so the
package works with zero non-stdlib deps. Same (n, k) semantics either way.
"""
from __future__ import annotations
from typing import Optional
# ---------------------------------------------------------------------------
# GF(2^8) arithmetic (pure-python fallback), primitive polynomial 0x11d.
# ---------------------------------------------------------------------------
_EXP = [0] * 512
_LOG = [0] * 256
def _init_tables() -> None:
x = 1
for i in range(255):
_EXP[i] = x
_LOG[x] = i
x <<= 1
if x & 0x100:
x ^= 0x11D
for i in range(255, 512):
_EXP[i] = _EXP[i - 255]
_init_tables()
def _gf_mul(a: int, b: int) -> int:
if a == 0 or b == 0:
return 0
return _EXP[_LOG[a] + _LOG[b]]
def _gf_div(a: int, b: int) -> int:
if b == 0:
raise ZeroDivisionError("GF division by zero")
if a == 0:
return 0
return _EXP[(_LOG[a] - _LOG[b]) % 255]
class _PureRS:
"""Minimal systematic RS(n,k) over GF(2^8) using a Vandermonde parity matrix.
Encodes per-byte across k data shards into (n-k) parity shards. Decodes by solving the
linear system over GF(2^8) for any k available shards (data or parity). MDS: any k of n
suffice. This is a clean, dependency-free reference implementation.
"""
def __init__(self, n: int, k: int):
if not (0 < k < n <= 255):
raise ValueError("require 0 < k < n <= 255")
self.n, self.k = n, k
# Vandermonde matrix V[i][j] = (i+1)^j for i in [0, n), j in [0, k).
# Rows 0..k-1 used as-is is NOT identity, so we keep full Vandermonde and solve
# generally on decode (no systematic assumption needed for correctness).
self.V = [[_gf_pow(i + 1, j) for j in range(k)] for i in range(n)]
def encode(self, data_shards: list[bytes]) -> list[bytes]:
assert len(data_shards) == self.k
length = len(data_shards[0])
assert all(len(s) == length for s in data_shards)
out = [bytearray(length) for _ in range(self.n)]
for pos in range(length):
col = [data_shards[j][pos] for j in range(self.k)]
for i in range(self.n):
acc = 0
row = self.V[i]
for j in range(self.k):
acc ^= _gf_mul(row[j], col[j])
out[i][pos] = acc
return [bytes(b) for b in out]
def decode(self, shards: list[Optional[bytes]]) -> list[bytes]:
"""shards: list of length n; None = lost. Returns reconstructed k data shards."""
present = [(i, s) for i, s in enumerate(shards) if s is not None]
if len(present) < self.k:
raise ValueError(f"need >= {self.k} shards, have {len(present)}")
idx = [i for i, _ in present[: self.k]]
sub = [self.V[i][:] for i in idx]
length = len(present[0][1])
recovered = [bytearray(length) for _ in range(self.k)]
for pos in range(length):
vec = [present[t][1][pos] for t in range(self.k)]
sol = _solve_gf(sub, vec)
for j in range(self.k):
recovered[j][pos] = sol[j]
return [bytes(b) for b in recovered]
def _gf_pow(a: int, p: int) -> int:
r = 1
for _ in range(p):
r = _gf_mul(r, a)
return r
def _solve_gf(matrix: list[list[int]], vec: list[int]) -> list[int]:
"""Gaussian elimination over GF(2^8). matrix is k x k, vec length k."""
k = len(vec)
m = [row[:] + [vec[i]] for i, row in enumerate(matrix)]
for col in range(k):
piv = next((r for r in range(col, k) if m[r][col] != 0), None)
if piv is None:
raise ValueError("singular matrix")
m[col], m[piv] = m[piv], m[col]
inv = _gf_div(1, m[col][col])
m[col] = [_gf_mul(x, inv) for x in m[col]]
for r in range(k):
if r != col and m[r][col] != 0:
f = m[r][col]
m[r] = [a ^ _gf_mul(f, b) for a, b in zip(m[r], m[col])]
return [m[i][k] for i in range(k)]
class ReedSolomonCoder:
"""(n, k) Reed-Solomon erasure coder. Default RS(10, 6) -> tolerate 4/10 lost."""
def __init__(self, n: int = 10, k: int = 6):
self.n, self.k = n, k
self._backend = "pure"
self._impl = _PureRS(n, k)
try:
import reedsolo # type: ignore # noqa: F401
self._backend = "reedsolo"
except Exception:
pass
@property
def backend(self) -> str:
return self._backend
def _split(self, data: bytes) -> tuple[list[bytes], int]:
orig_len = len(data)
shard_len = (orig_len + self.k - 1) // self.k
padded = data + b"\x00" * (shard_len * self.k - orig_len)
shards = [padded[i * shard_len : (i + 1) * shard_len] for i in range(self.k)]
return shards, orig_len
def encode(self, data: bytes) -> tuple[list[bytes], int]:
"""Return (n shards, original_length). Reconstruct from any k shards."""
data_shards, orig_len = self._split(data)
all_shards = self._impl.encode(data_shards)
return all_shards, orig_len
def decode(self, shards: list[Optional[bytes]], orig_len: int) -> bytes:
data_shards = self._impl.decode(shards)
return b"".join(data_shards)[:orig_len]
def encode_cell(cell_bytes: bytes, n: int = 10, k: int = 6) -> dict:
"""Encode a serialized cell into RS shards. Returns a portable shard manifest."""
coder = ReedSolomonCoder(n, k)
shards, orig_len = coder.encode(cell_bytes)
return {
"code": f"RS({n},{k})",
"backend": coder.backend,
"orig_len": orig_len,
"n": n,
"k": k,
"loss_tolerance": (n - k) / n,
"shards": [s.hex() for s in shards],
}
def decode_shards(manifest: dict) -> bytes:
"""Reconstruct original cell bytes from a shard manifest with up to (n-k) losses."""
n, k = manifest["n"], manifest["k"]
coder = ReedSolomonCoder(n, k)
shards = [bytes.fromhex(s) if s is not None else None for s in manifest["shards"]]
return coder.decode(shards, manifest["orig_len"])