Skip to main content

javm_cap/
cap_hash.rs

1//! Cap-level hashing — delegates to SSZ `hash_tree_root`.
2//!
3//! Each cap variant gets its identity digest by walking the cap's SSZ
4//! `HashTreeRoot` tree (SHA-256 chunking, merkleization, Union
5//! `mix_in_selector` for variant domain separation). The five legacy
6//! kind tags `0x10..0x50` are replaced by the SSZ Union selectors on
7//! `Cap` (0..4); the per-variant byte protocol is replaced by the
8//! field-by-field SSZ encoding derived on `InstanceCap`, `ImageCap`,
9//! `DataCap`, `CNodeCap`, and `TypeCap`.
10//!
11//! Cap-hash values changed in the SSZ migration (SHA-256 over the
12//! merkleized cap shape vs Blake2b over hand-rolled byte
13//! concatenations). The JAR chain has no live state — this is fine.
14//!
15//! **Substitution invariants** preserved by hand-written
16//! `HashTreeRoot` impls (see [`crate::page::PageSlot`],
17//! [`crate::page::PageBytes`], [`crate::cap::CapHashOrRef`]):
18//!
19//! - `PageSlot::Loaded(p)` hashes identically to
20//!   `PageSlot::Missing(p.hash)` — a freshly-loaded page substitutes
21//!   for a missing page without changing the enclosing cap's hash.
22//! - `CapHashOrRef::Hash(h)` hashes to `h` exactly — a freshly-published
23//!   cap blob substitutes for a `CapRef` reference without changing the
24//!   enclosing cap's hash.
25//!
26//! **Unresolved refs panic**: `cap_hash` on a cap whose graph still
27//! contains `CapHashOrRef::Ref(_)` targets will panic. Callers must
28//! `settle` the cap graph first.
29//!
30//! **Image hash duplication**: `cap_hash(Cap::Image(...))` and
31//! `image_content_hash` (over the SCALE `Image` shape) produce
32//! different digests. They hash different types — the cap-resident
33//! `ImageCap` has a flatter layout than `Image`. This is an
34//! intentional boundary; the cache publishes by `cap_hash`, while
35//! `image_content_hash` is used for the image-hash chain protocol in
36//! `crate::image`.
37
38use super::cap::{Cap, CapHash};
39
40/// 32-byte content hash of `cap`. Walks the cap tree via SSZ
41/// `HashTreeRoot` with SHA-256 as the default digest.
42pub fn cap_hash(cap: &Cap) -> CapHash {
43    ssz::hash_tree_root(cap)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    use crate::cap::{CapHashOrRef, TypeCap};
51    use crate::cnode::CNodeCap;
52    use crate::data::{DataCap, DataContent};
53    use crate::image_cap::ImageCap;
54    use crate::instance::InstanceCap;
55    use crate::page::{PageBytes, PageRef, PageSlot};
56    use crate::slot::SlotIdx;
57
58    #[test]
59    fn type_cap_hash_deterministic() {
60        let chain = [0xAA; 32];
61        let a: Cap = Cap::Type(TypeCap {
62            image_hash_chain: chain,
63        });
64        let b: Cap = Cap::Type(TypeCap {
65            image_hash_chain: chain,
66        });
67        assert_eq!(cap_hash(&a), cap_hash(&b));
68        // Different chain → different hash.
69        let c: Cap = Cap::Type(TypeCap {
70            image_hash_chain: [0xBB; 32],
71        });
72        assert_ne!(cap_hash(&a), cap_hash(&c));
73    }
74
75    #[test]
76    fn cap_variants_have_distinct_hashes() {
77        // The Union mix_in_selector ensures two caps whose payloads
78        // happen to merkleize to the same root still differ at the
79        // outer hash. Use the simplest distinguishable payloads.
80        let t: Cap = Cap::Type(TypeCap {
81            image_hash_chain: [0; 32],
82        });
83        let cn: Cap = Cap::CNode(CNodeCap::new(0).unwrap());
84        assert_ne!(cap_hash(&t), cap_hash(&cn));
85    }
86
87    #[test]
88    fn data_inline_hash_includes_size() {
89        let bytes_a: Vec<u8> = b"abc".to_vec();
90        let bytes_b: Vec<u8> = b"abc".to_vec();
91        // Two caps with different inline byte lengths (same prefix)
92        // hash differently because content storage IS the identifier.
93        // Pad to distinct page-multiple sizes.
94        let mut bytes_a_padded: Vec<u8> = vec![0u8; crate::data::PAGE_SIZE];
95        bytes_a_padded[..bytes_a.len()].copy_from_slice(bytes_a.as_slice());
96        let mut bytes_b_padded: Vec<u8> = vec![0u8; crate::data::PAGE_SIZE * 2];
97        bytes_b_padded[..bytes_b.len()].copy_from_slice(bytes_b.as_slice());
98        let a: Cap = Cap::Data(DataCap {
99            content: DataContent::Inline(bytes_a_padded),
100        });
101        let b: Cap = Cap::Data(DataCap {
102            content: DataContent::Inline(bytes_b_padded),
103        });
104        assert_ne!(cap_hash(&a), cap_hash(&b));
105    }
106
107    #[test]
108    fn cnode_empty_vs_one_populated_differ() {
109        let empty: CNodeCap = CNodeCap::new(2).unwrap();
110        let mut populated: CNodeCap = CNodeCap::new(2).unwrap();
111        populated
112            .set(SlotIdx(0), Some(CapHashOrRef::Hash([0xEE; 32])))
113            .unwrap();
114        let a: Cap = Cap::CNode(empty);
115        let b: Cap = Cap::CNode(populated);
116        assert_ne!(cap_hash(&a), cap_hash(&b));
117    }
118
119    #[test]
120    fn cnode_with_ref_target_panics() {
121        use crate::cap::CapRef;
122        let mut cn: CNodeCap = CNodeCap::new(2).unwrap();
123        cn.set(SlotIdx(0), Some(CapHashOrRef::Ref(CapRef::new(42))))
124            .unwrap();
125        let cap: Cap = Cap::CNode(cn);
126        let result = std::panic::catch_unwind(|| cap_hash(&cap));
127        assert!(result.is_err());
128    }
129
130    #[test]
131    fn image_hash_depends_on_code() {
132        let mut img_a = empty_image();
133        let mut img_b = empty_image();
134        img_a.code.extend_from_slice(b"foo");
135        img_b.code.extend_from_slice(b"bar");
136        let a: Cap = Cap::Image(img_a);
137        let b: Cap = Cap::Image(img_b);
138        assert_ne!(cap_hash(&a), cap_hash(&b));
139    }
140
141    fn empty_image() -> ImageCap {
142        ImageCap {
143            code: Vec::new(),
144            bitmask: Vec::new(),
145            jump_table: Vec::new(),
146            endpoints: Vec::new(),
147            mappings: Vec::new(),
148            pinned: Vec::new(),
149            initial: Vec::new(),
150            yield_marker_slot: None,
151        }
152    }
153
154    #[test]
155    fn instance_hash_depends_on_pc() {
156        let mut inst_a = empty_instance();
157        let mut inst_b = empty_instance();
158        inst_a.pc = 0x100;
159        inst_b.pc = 0x200;
160        let a: Cap = Cap::Instance(inst_a);
161        let b: Cap = Cap::Instance(inst_b);
162        assert_ne!(cap_hash(&a), cap_hash(&b));
163    }
164
165    fn empty_instance() -> InstanceCap {
166        InstanceCap {
167            image_hash_chain: [0; 32],
168            image_hash: [0; 32],
169            root_cnode: CapHashOrRef::Hash([0; 32]),
170            rw_overlays: Vec::new(),
171            mem_size: 0,
172            regs: [0; crate::cap::NUM_REGS],
173            pc: 0,
174            gas_remaining: 0,
175        }
176    }
177
178    #[test]
179    fn data_paged_hash_uses_loaded_page_hashes() {
180        let mut bytes = Vec::new();
181        bytes.extend_from_slice(&[1, 2, 3]);
182        let pb_hash = [0xA1; 32];
183        let pb = PageBytes {
184            hash: pb_hash,
185            bytes,
186        };
187        let pr: PageRef = PageRef::new(pb);
188        let pages: Vec<PageSlot> = vec![PageSlot::Loaded(pr)];
189        let cap: Cap = Cap::Data(DataCap {
190            content: DataContent::Paged {
191                page_size: 4096,
192                pages,
193            },
194        });
195        let h = cap_hash(&cap);
196        // Sanity: identical Cap shape with a different page hash differs.
197        let bytes2: Vec<u8> = vec![1, 2, 3];
198        let pb2 = PageBytes {
199            hash: [0xB2; 32],
200            bytes: bytes2,
201        };
202        let pr2: PageRef = PageRef::new(pb2);
203        let pages2: Vec<PageSlot> = vec![PageSlot::Loaded(pr2)];
204        let cap2: Cap = Cap::Data(DataCap {
205            content: DataContent::Paged {
206                page_size: 4096,
207                pages: pages2,
208            },
209        });
210        assert_ne!(h, cap_hash(&cap2));
211    }
212
213    #[test]
214    fn loaded_page_substitutes_for_missing_with_same_hash() {
215        // Substitution invariant: Loaded(p) and Missing(p.hash) must
216        // produce the same enclosing-cap hash.
217        let page_hash = [0xCD; 32];
218        let bytes: Vec<u8> = vec![0xAA; 16];
219        let pb = PageBytes {
220            hash: page_hash,
221            bytes,
222        };
223        let pr: PageRef = PageRef::new(pb);
224
225        let pages_loaded: Vec<PageSlot> = vec![PageSlot::Loaded(pr)];
226        let cap_loaded: Cap = Cap::Data(DataCap {
227            content: DataContent::Paged {
228                page_size: 16,
229                pages: pages_loaded,
230            },
231        });
232
233        let pages_missing: Vec<PageSlot> = vec![PageSlot::Missing(page_hash)];
234        let cap_missing: Cap = Cap::Data(DataCap {
235            content: DataContent::Paged {
236                page_size: 16,
237                pages: pages_missing,
238            },
239        });
240
241        assert_eq!(cap_hash(&cap_loaded), cap_hash(&cap_missing));
242    }
243}