khtst-rust / src /checkpoint.rs
PowerMachine's picture
v0.10.0: análise/prova/teste das críticas de engenharia Qwen3.8 lote 3 (r-35) — divisão SRP dos monólitos jepa/pensamento/moe/python em 26 submódulos (API estável via re-exports; 'Amdahl de compilação' refutada por experimento controlado, builds frios neutros), checkpoints atômicos SHA-256 puro FIPS 180-4 + fsync + rename + manifesto (CP-1..CP-3; correções da proposta: sem destruir estado anterior, manifesto com barreira, sem bincode/unwrap), TemperaturaInversa T-V3 (fronteira tipada da convexidade N-2; alegação compile-time corrigida), trait CamadaKhtst+CamadaHomeostatica (premissa do controlador refutada no fonte; Vec<Var>; adaptador MoeComPapel bit-a-bit), CLI Typer 75 (fachada aditiva com resume verificado por hash) — 123/123 cargo, 107/107 gates, treino real Wikipédia PT-BR com checkpoints íntegros
06e4b6f verified
Raw History Blame Contribute Delete
15.5 kB
//! Checkpoints atômicos com prova de integridade criptográfica (v0.10.0).
//!
//! **Crítica Qwen3.8 lote 3, nº 3 ("Checkpointing Não-Atômico") — ACEITA
//! COM CORREÇÕES (r-35 §4).** A proposta original tinha três falhas:
//!
//! 1. **Destruição do estado anterior:** `File::create(caminho_pesos)`
//! trunca o checkpoint ANTERIOR antes de escrever o novo — uma queda
//! de energia no meio da escrita perde AMBOS. A atomicidade real exige
//! escrita em `.tmp` + `rename(2)` (o POSIX garante troca atômica).
//! 2. **Manifesto sem barreira:** `fs::write(caminho_meta, ...)` sem
//! `sync_all` — o manifesto pode não atingir o disco quando os pesos
//! já atingiram (ou vice-versa), violando a própria ordenação alegada.
//! 3. **Dependência nova (bincode) + `unwrap()`** em caminho de I/O.
//!
//! Esta implementação: SHA-256 **puro em Rust** (FIPS 180-4, sem crates
//! externas — crates.io inacessível no ambiente), escrita tmp→fsync→
//! rename→fsync(dir) para AMBOS os arquivos, e manifesto lado-a-lado
//! `{caminho}.manifest.json` com hash + tamanho.
//!
//! **Teo CP-1 (atomicidade da troca).** `rename(2)` é atômico em relação
//! a leitores concorrentes e crashes: em qualquer instante o caminho
//! resolve OU para o arquivo antigo OU para o novo, nunca uma mistura.
//! Logo um crash em qualquer ponto deixa o caminho final = checkpoint
//! antigo (crash antes do rename) ou novo (depois). ∎ (garantia POSIX)
//!
//! **Teo CP-2 (durabilidade ordenada).** Com a disciplina
//! `write(W.tmp) → fsync(W.tmp) → rename(W) → write(M.tmp) →
//! fsync(M.tmp) → rename(M) → fsync(dir)`, após reboot: se `M` existe,
//! então o rename de `W` foi durável (executado e journalizado antes) e
//! os blocos de dados de `W` foram forçados por `fsync(W.tmp)` — portanto
//! `SHA-256(W) = M.hash` (a menos de colisão, CP-3). Se `M` não existe,
//! o estado é tratado como INVÁLIDO (não silenciosamente errado). ∎
//!
//! **Teo CP-3 (detecção de corrupção).** Para W' ≠ W com |W'| = |W|,
//! `P[SHA-256(W') = SHA-256(W)] ≤ 2^-256` (ataque direto) e, para
//! qualquer par distinto, ≤ 2^-128 (limite de aniversário). Truncamentos
//! e gravações parciais são capturados DETERMINISTICAMENTE pelo campo
//! `tamanho`. Nota honesta: a alegação original "matematicamente
//! idêntico" é exagerada — o correto é indistinguibilidade computacional
//! com probabilidade de colisão ≤ 2^-128. ∎
//!
//! **Compatibilidade (regra da casa: nada é removido):** checkpoints da
//! v0.9.0 (sem manifesto) continuam carregáveis pelos `carregar` legados;
//! `carregar_verificado` exige manifesto e recusa o contrário.
use candle_core::Result;
use std::fs::File;
use std::io::Write;
use std::path::Path;
pub const VERSAO_CHECKPOINT: &str = "0.10.0";
// ---------------------------------------------------------------------------
// SHA-256 — FIPS 180-4, implementação autocontida (sem dependências)
// ---------------------------------------------------------------------------
const K_SHA256: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
];
/// SHA-256 de `dados` — 32 bytes (FIPS 180-4 §6.2.2).
pub fn sha256(dados: &[u8]) -> [u8; 32] {
let mut h: [u32; 8] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
];
// padding: 0x80, zeros, comprimento de bit big-endian em 64 bits
let bitlen = (dados.len() as u64).wrapping_mul(8);
let mut msg = dados.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bitlen.to_be_bytes());
for bloco in msg.chunks_exact(64) {
let mut w = [0u32; 64];
for (i, j) in (0..64).step_by(4).enumerate() {
w[i] = u32::from_be_bytes([bloco[j], bloco[j + 1], bloco[j + 2], bloco[j + 3]]);
}
for t in 16..64 {
let s0 = w[t - 15].rotate_right(7) ^ w[t - 15].rotate_right(18) ^ (w[t - 15] >> 3);
let s1 = w[t - 2].rotate_right(17) ^ w[t - 2].rotate_right(19) ^ (w[t - 2] >> 10);
w[t] = w[t - 16]
.wrapping_add(s0)
.wrapping_add(w[t - 7])
.wrapping_add(s1);
}
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
for t in 0..64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let t1 = hh
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K_SHA256[t])
.wrapping_add(w[t]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let t2 = s0.wrapping_add(maj);
hh = g; g = f; f = e; e = d.wrapping_add(t1);
d = c; c = b; b = a; a = t1.wrapping_add(t2);
}
h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e); h[5] = h[5].wrapping_add(f);
h[6] = h[6].wrapping_add(g); h[7] = h[7].wrapping_add(hh);
}
let mut saida = [0u8; 32];
for (i, x) in h.iter().enumerate() {
saida[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
}
saida
}
/// SHA-256 em hexadecimal minúsculo (64 caracteres).
pub fn sha256_hex(dados: &[u8]) -> String {
sha256(dados).iter().map(|b| format!("{b:02x}")).collect()
}
// ---------------------------------------------------------------------------
// salva / carrega atômicos
// ---------------------------------------------------------------------------
/// Caminho do manifesto lado-a-lado de `caminho`.
pub fn caminho_manifesto(caminho: &str) -> String {
format!("{caminho}.manifest.json")
}
fn err_io(e: std::io::Error, ctx: &str) -> candle_core::Error {
candle_core::Error::Msg(format!("{ctx}: {e}"))
}
/// fsync de um diretório (durabilidade do rename — POSIX). Unix apenas;
/// em outras plataformas é no-op documentado.
fn fsync_dir(dir: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
let d = File::open(dir)?;
d.sync_all()?;
}
#[cfg(not(unix))]
{
let _ = dir;
}
Ok(())
}
/// Escrita atômica de `conteudo` em `caminho` com fsync + rename e
/// manifesto `{caminho}.manifest.json` (hash SHA-256 + tamanho).
///
/// Ordem (Teo CP-2): write(tmp) → fsync(tmp) → rename(final) para o
/// conteúdo e para o manifesto; fsync(dir) ao final.
pub fn salvar_atomico(caminho: &str, conteudo: &str) -> Result<()> {
let dados = conteudo.as_bytes();
let hash = sha256_hex(dados);
let tmp = format!("{caminho}.tmp");
// 1) conteúdo: tmp → fsync → rename (Teo CP-1: troca atômica)
{
let mut f = File::create(&tmp).map_err(|e| err_io(e, "checkpoint: criar tmp"))?;
f.write_all(dados).map_err(|e| err_io(e, "checkpoint: escrever tmp"))?;
f.sync_all().map_err(|e| err_io(e, "checkpoint: fsync tmp"))?;
}
std::fs::rename(&tmp, caminho).map_err(|e| err_io(e, "checkpoint: rename"))?;
// 2) manifesto: mesma disciplina (a proposta original usava fs::write
// simples — sem barreira, o manifesto pode perder-se sozinho)
let manifest = serde_json::json!({
"versao": VERSAO_CHECKPOINT,
"algoritmo": "sha256",
"hash_sha256": hash,
"tamanho": dados.len(),
});
let mtxt = serde_json::to_string(&manifest)
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
let mtmp = format!("{}.manifest.json.tmp", caminho);
{
let mut f = File::create(&mtmp).map_err(|e| err_io(e, "manifesto: criar tmp"))?;
f.write_all(mtxt.as_bytes()).map_err(|e| err_io(e, "manifesto: escrever tmp"))?;
f.sync_all().map_err(|e| err_io(e, "manifesto: fsync tmp"))?;
}
std::fs::rename(&mtmp, caminho_manifesto(caminho))
.map_err(|e| err_io(e, "manifesto: rename"))?;
// 3) durabilidade dos renames (POSIX: rename journalizado no dir)
if let Some(pai) = Path::new(caminho).parent() {
fsync_dir(pai).map_err(|e| err_io(e, "checkpoint: fsync dir"))?;
}
Ok(())
}
/// Lê `caminho` verificando o manifesto (hash + tamanho). Recusa
/// manifesto ausente ou divergente — Teo CP-3.
pub fn carregar_verificado(caminho: &str) -> Result<String> {
let dados = std::fs::read(caminho).map_err(|e| err_io(e, "checkpoint: ler"))?;
let mtxt = std::fs::read_to_string(caminho_manifesto(caminho))
.map_err(|_| candle_core::Error::Msg(format!(
"checkpoint sem manifesto (legado v0.9.0?): {caminho}")))?;
let m: serde_json::Value = serde_json::from_str(&mtxt)
.map_err(|e| candle_core::Error::Msg(format!("manifesto ilegível: {e}")))?;
let hash_esp = m["hash_sha256"].as_str().unwrap_or("");
let tam_esp = m["tamanho"].as_u64().unwrap_or(u64::MAX);
let hash_rec = sha256_hex(&dados);
if (dados.len() as u64) != tam_esp {
candle_core::bail!(
"checkpoint corrompido: tamanho {} ≠ manifesto {} ({caminho})",
dados.len(), tam_esp);
}
if hash_rec != hash_esp {
candle_core::bail!(
"checkpoint corrompido: SHA-256 divergente ({caminho})");
}
String::from_utf8(dados)
.map_err(|e| candle_core::Error::Msg(format!("utf8: {e}")))
}
/// Verificação sem exceção (para uso em Python/CLI antes de resume).
pub fn validar(caminho: &str) -> bool {
carregar_verificado(caminho).is_ok()
}
#[cfg(test)]
mod testes_checkpoint {
use super::*;
fn tmp_dir(nome: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("khtst_cp_{nome}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
/// CP-0a — SHA-256 contra os vetores oficiais FIPS 180-4.
#[test]
fn cp0a_sha256_vetores_fips() {
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
assert_eq!(
sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
assert_eq!(
sha256_hex(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
// vetor de 4 blocos (256 bytes) — exercita o laço de blocos
let longo = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn".repeat(4);
assert_eq!(sha256_hex(longo.as_bytes()).len(), 64);
}
/// CP-1a — salvar produz final + manifesto; tmp não permanece;
/// carregar_verificado devolve o conteúdo bit-a-bit.
#[test]
fn cp1a_salvar_carregar_roundtrip() {
let d = tmp_dir("rt");
let p = d.join("ck.json");
salvar_atomico(p.to_str().unwrap(), "{\"v0\":[1.0,2.0]}").unwrap();
assert!(p.exists());
assert!(d.join("ck.json.manifest.json").exists());
assert!(!d.join("ck.json.tmp").exists());
assert!(!d.join("ck.json.manifest.json.tmp").exists());
assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(),
"{\"v0\":[1.0,2.0]}");
let _ = std::fs::remove_dir_all(&d);
}
/// CP-3a — adulteração de 1 byte é detectada (hash divergente).
#[test]
fn cp3a_adulteracao_detectada() {
let d = tmp_dir("t");
let p = d.join("ck.json");
salvar_atomico(p.to_str().unwrap(), "conteudo original").unwrap();
std::fs::write(&p, "conteudo originalX").unwrap();
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
assert!(!validar(p.to_str().unwrap()));
let _ = std::fs::remove_dir_all(&d);
}
/// CP-3b — truncamento detectado pelo tamanho (determinístico).
#[test]
fn cp3b_truncamento_detectado() {
let d = tmp_dir("tr");
let p = d.join("ck.json");
salvar_atomico(p.to_str().unwrap(), "0123456789").unwrap();
std::fs::write(&p, "0123").unwrap();
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
let _ = std::fs::remove_dir_all(&d);
}
/// CP-4a — manifesto ausente ⇒ recusa (estado legado é do `carregar`).
#[test]
fn cp4a_manifesto_ausente_recusado() {
let d = tmp_dir("leg");
let p = d.join("ck.json");
salvar_atomico(p.to_str().unwrap(), "x").unwrap();
std::fs::remove_file(d.join("ck.json.manifest.json")).unwrap();
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
let _ = std::fs::remove_dir_all(&d);
}
/// CP-1b — falha de escrita PRESERVA o checkpoint anterior
/// (atomicidade prática: dir somente-leitura impede o create do tmp).
#[test]
fn cp1b_falha_preserva_anterior() {
let d = tmp_dir("fa");
let p = d.join("ck.json");
salvar_atomico(p.to_str().unwrap(), "estado_bom").unwrap();
// dir somente-leitura: File::create(tmp) falha
let mut perms = std::fs::metadata(&d).unwrap().permissions();
#[allow(clippy::unnecessary_cast)]
{
use std::os::unix::fs::PermissionsExt;
perms.set_mode(0o555);
}
std::fs::set_permissions(&d, perms).unwrap();
let r = salvar_atomico(p.to_str().unwrap(), "estado_novo");
let mut perms2 = std::fs::metadata(&d).unwrap().permissions();
#[allow(clippy::unnecessary_cast)]
{
use std::os::unix::fs::PermissionsExt;
perms2.set_mode(0o755);
}
std::fs::set_permissions(&d, perms2).unwrap();
assert!(r.is_err(), "escrita deveria falhar em dir somente-leitura");
// o anterior permanece ÍNTEGRO e válido (Teo CP-1)
assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(), "estado_bom");
let _ = std::fs::remove_dir_all(&d);
}
/// CP-5a — manifesto trocado entre arquivos (hash de outro) recusa.
#[test]
fn cp5a_manifesto_de_outro_recusado() {
let d = tmp_dir("sw");
let a = d.join("a.json");
let b = d.join("b.json");
salvar_atomico(a.to_str().unwrap(), "AAAA").unwrap();
salvar_atomico(b.to_str().unwrap(), "BBBB").unwrap();
std::fs::copy(d.join("b.json.manifest.json"), d.join("a.json.manifest.json")).unwrap();
assert!(carregar_verificado(a.to_str().unwrap()).is_err());
let _ = std::fs::remove_dir_all(&d);
}
}