From 8b2ff3cfd4747f0496f77f392a3e40dd5e49d714 Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 1 Sep 2026 19:10:07 +0000 Subject: [PATCH] =?UTF-8?q?digest:=20blake3=20pelado,=20no=20length-prefij?= =?UTF-8?q?ado=20=E2=80=94=20el=20mismo=20archivo=20ten=C3=ADa=20dos=20hex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lo destapó escribir la contraparte del ADR 0014 para churay: churay direcciona sus blobs con blake3(bytes) pelado y hammer iba a sellar el digest con of_inputs(&[bytes]), los dos bajo el prefijo `b3:`. El mismo archivo con dos hex distintos, y un CAS compartido que no falla ruidosamente: cada lado busca un nombre distinto para los mismos bytes. Con una sola entrada el length-prefijado no desambigua ninguna concatenación — sólo hace que el nombre deje de ser verificable por un tercero con b3sum en la mano. `ArtifactHash::of_bytes` YA EXISTÍA y ya era blake3 pelado: es la convención de of_file y la del expected_hash de un .swm, así que of_inputs era además la pieza fuera de sitio dentro del propio repo. of_inputs se queda para lo que fue escrito: hashear una LISTA. Coste: una línea, porque ningún índice publicado lleva todavía el campo. Es el argumento del ADR aplicado a sí mismo — decidir antes de que haya usuarios. La corrección queda en el ADR, no reescrita en silencio. Guardián: digest_es_blake3_pelado_y_no_length_prefijado, con vector fijo de b3sum y un assert_ne contra of_inputs que nombra la consecuencia. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR --- crates/hammer-core/src/hash.rs | 7 +++++ crates/hammer-core/src/repo.rs | 34 +++++++++++++++++++---- docs/adr/0014-distribucion-multiorigen.md | 19 +++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/hammer-core/src/hash.rs b/crates/hammer-core/src/hash.rs index 03b7f37f..39cf3784 100644 --- a/crates/hammer-core/src/hash.rs +++ b/crates/hammer-core/src/hash.rs @@ -101,6 +101,13 @@ impl ArtifactHash { /// BLAKE3 crudo de un buffer en memoria — exactamente `blake3(bytes)`, la misma convención que /// [`of_file`](Self::of_file). Lo usa el checker de evidencia (H1b) para anclar el stdout de un /// check contra su `expected_output`: quien recompute `blake3` de la salida obtiene el mismo `b3:…`. + /// + /// **Ésta, y no [`of_inputs`](Self::of_inputs), es la que direcciona un blob por su contenido** + /// (ADR 0014): con una sola entrada el length-prefijado no desambigua nada y cuesta que el + /// nombre deje de ser verificable por un tercero con `b3sum` en la mano. Vale también fuera del + /// repo: churay (tawasuyu) direcciona sus blobs con esta misma función bajo el mismo prefijo + /// `b3:`, así que un CAS compartido sólo cierra si los dos lados usan ésta — con `of_inputs` el + /// mismo archivo tendría dos hex distintos y la discrepancia no fallaría ruidosamente. pub fn of_bytes(bytes: &[u8]) -> ArtifactHash { ArtifactHash(format!("b3:{}", blake3::hash(bytes).to_hex())) } diff --git a/crates/hammer-core/src/repo.rs b/crates/hammer-core/src/repo.rs index 1da8652c..bcf8e211 100644 --- a/crates/hammer-core/src/repo.rs +++ b/crates/hammer-core/src/repo.rs @@ -23,12 +23,13 @@ pub const INDEX_FILE: &str = "index.json"; /// El `digest` de unos bytes de `.swm`, en el mismo formato `b3:…` que un [`crate::ArtifactHash`]. /// -/// Length-prefijado como todo lo demás: la función es la misma que sella artefactos, no una segunda -/// convención de hash conviviendo con la primera. +/// **BLAKE3 pelado** ([`ArtifactHash::of_bytes`](crate::ArtifactHash::of_bytes)), no +/// `of_inputs(&[bytes])`: un blob se direcciona con el hash que cualquiera obtiene de esos bytes — +/// el de `b3sum` — o el nombre deja de ser verificable por un tercero. Es además el mismo que usa +/// churay para sus blobs, y con `of_inputs` el mismo archivo tendría dos hex distintos bajo el +/// mismo prefijo en los dos proyectos. pub fn digest_swm(bytes: &[u8]) -> String { - crate::ArtifactHash::of_inputs(&[bytes]) - .as_str() - .to_string() + crate::ArtifactHash::of_bytes(bytes).as_str().to_string() } /// Comprueba que `bytes` casan con el `digest` que el índice **firmado** declara para esa entrada. @@ -269,6 +270,29 @@ mod tests { ); } + /// Guardián de interoperabilidad: el `digest` tiene que ser el BLAKE3 que obtiene CUALQUIERA + /// de esos bytes (`b3sum`), no `of_inputs(&[bytes])`. Es la §5 del SDD de churay: el mismo + /// archivo con dos hex distintos bajo el mismo prefijo `b3:` no falla ruidosamente — los dos + /// lados buscan nombres distintos para los mismos bytes. + #[test] + fn digest_es_blake3_pelado_y_no_length_prefijado() { + let bytes = b"contenido del .swm"; + assert_eq!( + digest_swm(bytes), + crate::ArtifactHash::of_bytes(bytes).as_str() + ); + assert_ne!( + digest_swm(bytes), + crate::ArtifactHash::of_inputs(&[bytes.as_slice()]).as_str(), + "volver a of_inputs rompe el CAS compartido con churay, y en silencio" + ); + // Vector fijo: si `of_bytes` cambiara de convención, esto lo caza igual. + assert_eq!( + digest_swm(b"abc"), + "b3:6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85" + ); + } + #[test] fn digest_ausente_no_cambia_la_serializacion() { // `skip_serializing_if` ⇒ un índice ya firmado antes de este campo serializa IDÉNTICO y su diff --git a/docs/adr/0014-distribucion-multiorigen.md b/docs/adr/0014-distribucion-multiorigen.md index f6c49281..8526acd4 100644 --- a/docs/adr/0014-distribucion-multiorigen.md +++ b/docs/adr/0014-distribucion-multiorigen.md @@ -100,10 +100,25 @@ alcanza: `expected_hash` es **opcional**, y el `.swm` se lee mucho antes de lleg colisiones de ficheros de `install` ya decide con su contenido). El comentario de `download.rs` decía que el `patch_url` «se cubre indirectamente»; *indirectamente* no es una cadena de confianza. -Se cierra con `PackageEntry::digest` (BLAKE3, `b3:…`, la misma función que sella artefactos — no una -segunda convención de hash conviviendo con la primera), sellado al publicar y verificado antes de +Se cierra con `PackageEntry::digest` (BLAKE3, `b3:…`), sellado al publicar y verificado antes de escribir un byte. La cadena queda: **clave raíz → firma del índice → `digest` → bytes**. +> **CORRECCIÓN (mismo día).** Este párrafo decía que el `digest` usaba «la misma función que sella +> artefactos», o sea `of_inputs(&[bytes])` — length-prefijado. **Estaba mal, y la razón importa.** +> Con una sola entrada el length-prefijado no desambigua ninguna concatenación: sólo hace que +> `b3:` **deje de ser el hash que obtiene un tercero** con `b3sum` sobre el mismo archivo. +> +> Lo destapó escribir la contraparte de este ADR para churay (`02_ruway/churay/SDD-DISTRIBUCION.md` +> §5 en tawasuyu): churay direcciona sus blobs con `blake3(bytes)` pelado y hammer iba a hacerlo +> length-prefijado, **bajo el mismo prefijo `b3:`** ⇒ el mismo archivo con dos hex distintos, y un +> CAS compartido que no falla ruidosamente sino que busca nombres distintos para los mismos bytes. +> +> Ahora usa `ArtifactHash::of_bytes` — que **ya existía en hammer** y ya era blake3 pelado; es la +> convención de `of_file` y la del `expected_hash` de un `.swm`, así que `of_inputs` era además la +> pieza fuera de sitio *dentro* del propio repo. Coste del cambio: una línea, porque ningún índice +> publicado llevaba todavía el campo. Es el argumento de este ADR aplicado a sí mismo — decidir +> antes de que haya usuarios. Hay guardián: `digest_es_blake3_pelado_y_no_length_prefijado`. + El campo es `Option` con `skip_serializing_if`, así que **un índice firmado antes de este cambio serializa idéntico y su firma sigue siendo válida** (hay test). Un índice sin digests no bloquea, pero **dice cuántos paquetes no verificó**: un índice viejo servido desde un espejo ajeno no es