open-standards/map/map-rs/crates/map-conformance/tests/pack.rs
Transaction Science fef51e4ecd
Some checks failed
standards workspaces build + test / jouleclaw-rs (push) Has been cancelled
standards workspaces build + test / aac-rs (push) Has been cancelled
standards workspaces build + test / arl-rs (push) Has been cancelled
standards workspaces build + test / eoc-rs (push) Has been cancelled
standards workspaces build + test / jcp-rs (push) Has been cancelled
standards workspaces build + test / joule-code-rs (push) Has been cancelled
standards workspaces build + test / joule-ui-rs (push) Has been cancelled
standards workspaces build + test / joulecontract-rs (push) Has been cancelled
standards workspaces build + test / jouledesk-rs (push) Has been cancelled
standards workspaces build + test / joulehook-rs (push) Has been cancelled
standards workspaces build + test / map-rs (push) Has been cancelled
standards workspaces build + test / mesh-rs (push) Has been cancelled
standards workspaces build + test / openpay (push) Has been cancelled
standards workspaces build + test / proof-rs (push) Has been cancelled
standards workspaces build + test / sandbox-rs (push) Has been cancelled
standards workspaces build + test / sense-rs (push) Has been cancelled
standards workspaces build + test / smart-byte-rs (push) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (push) Has been cancelled
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Has been cancelled
map: an unbounded band width reads back from JSON, so the published tileset round-trips
AltSchedule::default() — the surface-only schedule, the common case — has width_m
= +inf. JSON has no infinity: serde_json wrote null and f64 refused it coming back,
so tileset_vectors.json could not be read into the reference types at all
("invalid type: null, expected f64") — the first thing a third party certifying
against the pack would try.

width_m now has one JSON form: +inf <-> null, in human-readable formats only. The
canonical CBOR every hash and signature covers still carries the float, so no
content id or signature moves and the published pack is byte-identical. NaN and
-inf, which mean nothing and have no JSON form, are refused rather than written
as a null that would read back as +inf. SPEC §2 states the rule.

The pack test that pinned the defect now asserts the round trip: the published
manifest reads back, verifies, re-serializes to the pack, equals the rebuilt
manifest, and the revision chains onto it. Falsified: without the adapter the
original error returns; writing null into CBOR too breaks the canonical-form test
and the published signatures.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 14:33:21 -04:00

241 lines
11 KiB
Rust

//! Consume the published `map` conformance pack.
//!
//! The harness in `main.rs` runs its suites against in-process fixtures, and CI now
//! runs that harness — but nothing reads `map/conformance/v1/*.json`, the files a
//! third-party implementation is told to certify against. So the published bytes
//! could drift from the implementation with nothing to notice.
//!
//! Inputs come from the pack and recorded outputs are recomputed, not re-read:
//! a cell key is rebuilt from its scalar fields and re-addressed, a directory is
//! deserialized and its Merkle root recomputed, signatures are checked, and the
//! sealed receipt is re-derived and compared byte for byte.
//!
//! Note `gen-vectors` writes to a hardcoded path with no output argument, so it
//! cannot be run to diff against a scratch copy without overwriting the published
//! pack. This test does that comparison in-process instead.
use ed25519_dalek::SigningKey;
use map_receipt::{Energy, Receipt};
use map_schema::{
AltBand, AltSchedule, CellKey, ContentId, DirEntry, Directory, Lod, Manifest, ManifestBody,
Revision, RevisionBody, S2CellId, Tier, TimeBucket, canonical_cbor, verify_chain,
};
use serde_json::Value;
use std::path::PathBuf;
/// The fixed publisher key the pack is generated under.
const PUBLISHER_SEED: [u8; 32] = [7u8; 32];
fn pack(name: &str) -> Value {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../conformance/v1")
.canonicalize()
.expect("the conformance pack must exist next to the spec")
.join(name);
serde_json::from_slice(&std::fs::read(&p).unwrap_or_else(|e| panic!("read {name}: {e}")))
.unwrap_or_else(|e| panic!("{name} must parse: {e}"))
}
/// Every published cell key re-addresses to its recorded canonical CBOR and content
/// id, rebuilt from the scalar fields rather than from a fixture.
#[test]
fn published_cell_keys_re_address_to_their_recorded_ids() {
let doc = pack("cell_key_vectors.json");
let cells = doc["cells"].as_array().expect("pack lists cells");
assert!(cells.len() >= 3, "a wrong path must not look like a pass");
for (i, c) in cells.iter().enumerate() {
let key = CellKey {
s2: S2CellId(c["s2"].as_u64().unwrap()),
alt: AltBand(c["alt"].as_i64().unwrap() as _),
time: TimeBucket {
start_tai_ms: c["time"]["start_tai_ms"].as_i64().unwrap() as _,
width_ms: c["time"]["width_ms"].as_u64().unwrap() as _,
},
lod: Lod(c["lod"].as_u64().unwrap() as u8),
};
assert_eq!(
hex::encode(canonical_cbor(&key).unwrap()),
c["canonical_cbor_hex"].as_str().unwrap(),
"cell {i}: canonical CBOR differs from the published bytes"
);
assert_eq!(
ContentId::of(&key).unwrap().to_hex(),
c["content_id"].as_str().unwrap(),
"cell {i}: content id differs from the published id"
);
}
}
/// The tileset the pack publishes, rebuilt exactly as `gen-vectors` builds it.
///
/// Rebuilding is the stronger comparison — it holds the implementation to the
/// published bytes rather than merely reading them back. Reading them back is
/// checked too, in `the_published_tileset_round_trips`.
fn rebuilt_tileset() -> (Directory, Manifest, Revision) {
let sk = SigningKey::from_bytes(&PUBLISHER_SEED);
let cell = |s2: u64, lod: u8| CellKey {
s2: S2CellId(s2),
alt: AltBand(0),
time: TimeBucket::ATEMPORAL,
lod: Lod(lod),
};
let directory = Directory::new(vec![
DirEntry { key: cell(0x10, 0), offset: 0, len: 128, content_id: ContentId::of_bytes(b"a") },
DirEntry { key: cell(0x20, 0), offset: 128, len: 64, content_id: ContentId::of_bytes(b"b") },
DirEntry { key: cell(0x30, 0), offset: 256, len: 32, content_id: ContentId::of_bytes(b"c") },
]);
let manifest = ManifestBody {
spec_version: "0.1.0".into(),
datum: "WGS84".into(),
alt_schedule: AltSchedule::default(),
dir_root: directory.root().unwrap(),
bounds: Some((cell(0x10, 0), cell(0x30, 0))),
disclaimer: None,
publisher: sk.verifying_key(),
}
.sign(&sk)
.unwrap();
let revision = RevisionBody {
prev_root: manifest.root().unwrap(),
new_root: ContentId::of_bytes(b"next-cycle-root"),
timestamp_tai_ms: 1,
publisher: sk.verifying_key(),
}
.sign(&sk)
.unwrap();
(directory, manifest, revision)
}
/// The implementation still produces exactly the published tileset, its Merkle root
/// recomputes, both signatures verify, and the revision chains onto the manifest —
/// which is what makes the tileset updatable rather than merely signed.
#[test]
fn published_tileset_still_matches_and_its_signatures_verify() {
let doc = pack("tileset_vectors.json");
let (directory, manifest, revision) = rebuilt_tileset();
assert_eq!(serde_json::to_value(&directory).unwrap(), doc["directory"], "directory has drifted");
assert_eq!(serde_json::to_value(&manifest).unwrap(), doc["manifest"], "manifest has drifted");
assert_eq!(serde_json::to_value(&revision).unwrap(), doc["revision"], "revision has drifted");
assert_eq!(directory.root().unwrap().to_hex(), doc["dir_root"].as_str().unwrap());
manifest.verify().expect("the published manifest must verify");
revision.verify().expect("the published revision must verify");
assert_eq!(manifest.body.dir_root, directory.root().unwrap());
assert_eq!(revision.body.prev_root, manifest.root().unwrap());
let tip = verify_chain(manifest.root().unwrap(), std::slice::from_ref(&revision))
.expect("the one-link revision chain must verify");
assert_eq!(tip, revision.body.new_root);
}
/// The checks above are not vacuous.
///
/// Note what the root does *not* commit to: `root()` is documented as the Merkle
/// root over the sorted `(key, content_id)` leaves, so editing an entry's `offset`
/// or `len` leaves it unchanged by design. That is sound — the `content_id`
/// authenticates whatever bytes a range fetch returns, so a moved range fails at the
/// content check rather than at the root. The tamper below therefore edits a
/// `content_id`, which the root does commit to.
#[test]
fn tampering_with_committed_fields_is_rejected() {
let (mut directory, mut manifest, revision) = rebuilt_tileset();
let published_root = directory.root().unwrap();
directory.entries[0].content_id = ContentId::of_bytes(b"swapped tile");
assert_ne!(directory.root().unwrap(), published_root, "a swapped tile left the root unchanged");
manifest.body.datum = "NOT-WGS84".into();
assert!(manifest.verify().is_err(), "a rewritten manifest body still verified");
let wrong_root = ContentId::of_bytes(b"not the manifest root");
assert!(
verify_chain(wrong_root, std::slice::from_ref(&revision)).is_err(),
"the revision chained onto a root it does not follow"
);
}
/// The published tileset reads back into the reference implementation's own types —
/// what a third party certifying against the pack does first — and what it reads
/// back verifies, re-serializes to the published JSON, and has the rebuilt roots.
///
/// `AltSchedule::default()`, the schedule a 2.5D surface-only tileset uses and so the
/// common case, has `width_m = +∞`. JSON has no infinity, so the pack writes it as
/// `null`, and the schema reads `null` back as `+∞`. Until it did, the manifest could
/// not be read back at all (`invalid type: null, expected f64`). The signing form is
/// CBOR, which carries the float, so no signature or content id moved.
#[test]
fn the_published_tileset_round_trips() {
let doc = pack("tileset_vectors.json");
let (directory, rebuilt, _) = rebuilt_tileset();
assert!(
doc["manifest"]["body"]["alt_schedule"]["width_m"].is_null(),
"an unbounded band width is published as null"
);
let manifest: Manifest = serde_json::from_value(doc["manifest"].clone())
.expect("the published manifest must read back");
assert_eq!(manifest.body.alt_schedule.width_m, f64::INFINITY, "null must read back as +inf");
manifest.verify().expect("the manifest read from JSON must verify: its signed form is unchanged");
assert_eq!(serde_json::to_value(&manifest).unwrap(), doc["manifest"], "it must re-serialize to the pack");
assert_eq!(manifest, rebuilt, "it must be the manifest the implementation builds");
assert_eq!(manifest.root().unwrap(), rebuilt.root().unwrap());
let read_directory: Directory = serde_json::from_value(doc["directory"].clone())
.expect("the published directory must read back");
assert_eq!(read_directory.root().unwrap(), directory.root().unwrap());
let revision: Revision = serde_json::from_value(doc["revision"].clone())
.expect("the published revision must read back");
revision.verify().expect("and verify");
assert_eq!(revision.body.prev_root, manifest.root().unwrap(), "and chain onto the manifest read back");
}
/// Drift: the reference implementation still seals exactly the published receipt.
///
/// This is the check that caught stale vectors in joulehook and jouledesk, whose
/// published JCR-1 envelopes fell behind when the receipt payload gained a field.
/// `map` pins a JCR-1 envelope the same way, so it is exposed to the same drift.
#[test]
fn the_published_receipt_is_still_what_the_implementation_seals() {
let doc = pack("receipt_vectors.json");
let steward = doc["steward"].as_str().expect("pack names the steward");
let sk = SigningKey::from_bytes(&PUBLISHER_SEED);
assert_eq!(
hex::encode(sk.verifying_key().to_bytes()),
doc["publisher_pubkey"].as_str().unwrap(),
"the pack's publisher key is not the documented seed's"
);
// Rebuilt exactly as gen-vectors builds it.
let rcpt = Receipt {
cell: CellKey {
s2: S2CellId(0x42),
alt: AltBand(0),
time: TimeBucket::ATEMPORAL,
lod: Lod(0),
},
content_id: ContentId::of_bytes(b"tile"),
tier: Tier::Photoreal,
energy: Energy::estimate_tier(Tier::Photoreal),
sequence: 1,
nonce: [0, 0, 0, 0, 0, 0, 0, 1],
};
assert_eq!(
rcpt.to_jcr1_wire(steward).unwrap(),
doc["wire"],
"the JCR-1 wire projection differs from the published one"
);
assert_eq!(
hex::encode(rcpt.signed_core(steward).unwrap()),
doc["signed_core_hex"].as_str().unwrap(),
"the signed core differs from the published bytes"
);
assert_eq!(
hex::encode(rcpt.seal(&sk, steward).unwrap()),
doc["cose_sign1_envelope_hex"].as_str().unwrap(),
"the sealed envelope differs from the published bytes — a stale vector?"
);
}