thruput_contract_spec/format.rs
1//! Wire-format versioning for the raw spec ELF section.
2//!
3//! The raw section body is an unframed concatenation of XDR-encoded
4//! [`SpecFragment`](crate::SpecFragment) values, so a parser that disagrees
5//! with the producer about the fragment layout does not fail cleanly: it
6//! reads the field set it knows, stops short, and resumes decoding at a byte
7//! offset inside the previous fragment. Everything after that decodes as
8//! garbage, and nothing in the stream distinguishes "newer than me" from
9//! "corrupt".
10//!
11//! The section *name* carries the version so that disagreement is detectable
12//! before a single fragment byte is read. Producers emit
13//! [`RAW_SECTION_NAME`]; consumers match on [`RAW_SECTION_PREFIX`] and
14//! compare the suffix against [`RAW_SPEC_FORMAT`], which lets an unreadable
15//! artifact be reported as a version skew naming both sides rather than as a
16//! parse failure or a missing spec.
17//!
18//! # Bumping
19//!
20//! Any change to the encoding of `SpecFragment` or of a type reachable from
21//! it is a format change and must bump [`RAW_SPEC_FORMAT`]. Four mirrors
22//! must move together, and each is pinned by a test so a missed one fails CI
23//! rather than shipping:
24//!
25//! - this constant,
26//! - `[package.metadata.thruput] raw_spec_format` in this crate's
27//! `Cargo.toml`, which is how the `thruput` CLI reads the format version
28//! out of a contract's dependency graph before compiling it,
29//! - the `link_section` literals in `thruput-macros`,
30//! - the `(INFO)` output section in `thruput-rv32-runtime`'s
31//! `thruput-rv32.ld`, which keeps the section out of guest memory.
32
33/// Prefix shared by every version of the raw spec ELF section name.
34///
35/// Consumers match on this rather than on [`RAW_SECTION_NAME`] so that a
36/// section written by a different SDK is recognized as a raw spec section of
37/// the wrong version, instead of being skipped as unrelated (which surfaces
38/// as "this contract has no spec" — the wrong diagnosis).
39pub const RAW_SECTION_PREFIX: &str = "thruputspecraw";
40
41/// Wire-format version of the raw spec section this SDK produces and parses.
42pub const RAW_SPEC_FORMAT: u32 = 5;
43
44/// Name of the raw spec ELF section this SDK emits.
45///
46/// Kept as a literal rather than concatenated from [`RAW_SECTION_PREFIX`] and
47/// [`RAW_SPEC_FORMAT`] because `#[link_section = ...]` in `thruput-macros`
48/// needs a literal anyway; `raw_section_name_matches_its_parts` pins the
49/// three against each other.
50pub const RAW_SECTION_NAME: &str = "thruputspecrawv5";
51
52/// Name of the finalized spec ELF section.
53///
54/// It holds a `ContractSpec`, whose layout is governed by the protocol-level
55/// `thruput-spec` crate, so the suffix moves when that layout does. `v2`
56/// adds method effects; `v1` added segmented spec ids. Rebuild contracts and
57/// bindings after updating the toolchain; legacy decoding is not supported.
58///
59/// Unlike the raw section, the suffix is not parsed: [`classify_section`]
60/// matches this name exactly, so a `v0` section reads as
61/// [`SpecSection::Other`] and the artifact reports as having no spec rather
62/// than as a version skew. That is the wrong diagnosis, but it is a safe one
63/// -- the alternative, decoding `v0` bytes as `v1`, misaligns silently. A
64/// finalized section only ever reaches a reader in an unlowered build
65/// artifact: `thruput-rv32-lower` drops both spec sections, so nothing
66/// deployed carries one.
67pub const FINAL_SECTION_NAME: &str = "thruputspecv2";
68
69/// What an ELF section name means to the spec pipeline.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SpecSection {
72 /// A raw fragment section carrying the given wire-format version. The
73 /// version may differ from [`RAW_SPEC_FORMAT`]; callers must check.
74 Raw(u32),
75 /// The finalized spec section.
76 Final,
77 /// Not a spec section.
78 Other,
79}
80
81/// Classify an ELF section name.
82///
83/// A raw section whose suffix is not `v<integer>` classifies as
84/// [`SpecSection::Other`]: an unparseable suffix cannot be reported as a
85/// version skew, and treating it as a raw section would only turn it into a
86/// decode failure later.
87pub fn classify_section(name: &str) -> SpecSection {
88 if name == FINAL_SECTION_NAME {
89 return SpecSection::Final;
90 }
91 match name
92 .strip_prefix(RAW_SECTION_PREFIX)
93 .and_then(|suffix| suffix.strip_prefix('v'))
94 .and_then(|digits| digits.parse::<u32>().ok())
95 {
96 Some(version) => SpecSection::Raw(version),
97 None => SpecSection::Other,
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn raw_section_name_matches_its_parts() {
107 assert_eq!(
108 classify_section(RAW_SECTION_NAME),
109 SpecSection::Raw(RAW_SPEC_FORMAT),
110 "RAW_SECTION_NAME must be RAW_SECTION_PREFIX + 'v' + \
111 RAW_SPEC_FORMAT, or producers and consumers disagree",
112 );
113 }
114
115 #[test]
116 fn rv32_linker_script_keeps_raw_spec_out_of_guest_memory() {
117 // Without its own non-alloc output section the raw spec is an orphan
118 // ALLOC section that lld places in DATA, where it counts against the
119 // guest memory size the heap ASSERT checks. Whole trimmed lines are
120 // compared, so a typo on either side of the name fails, and nothing
121 // allocates, so the test builds without the `alloc` feature.
122 let script = include_str!("../../rv32-runtime/link/thruput-rv32.ld");
123 let has_line = |prefix: &str, suffix: &str| {
124 script.lines().any(|line| {
125 line.trim()
126 .strip_prefix(prefix)
127 .and_then(|rest| rest.strip_suffix(suffix))
128 == Some(RAW_SECTION_NAME)
129 })
130 };
131 assert!(
132 has_line("", " 0 (INFO) :") && has_line("KEEP(*(", "))"),
133 "thruput-rv32.ld must place {RAW_SECTION_NAME} in an (INFO) \
134 output section; update it alongside RAW_SPEC_FORMAT",
135 );
136 }
137
138 #[test]
139 fn manifest_metadata_matches_raw_spec_format() {
140 // The CLI reads this key out of a contract's resolved dependency
141 // graph via `cargo metadata` to detect skew before compiling, so it
142 // must track the constant. See the module docs on bumping.
143 let manifest = include_str!("../Cargo.toml");
144 let declared = manifest
145 .lines()
146 .find_map(|line| line.trim().strip_prefix("raw_spec_format"))
147 .and_then(|rest| rest.trim().strip_prefix('='))
148 .and_then(|rest| rest.trim().parse::<u32>().ok())
149 .expect(
150 "Cargo.toml must carry `[package.metadata.thruput] \
151 raw_spec_format = <n>` so `cargo metadata` reports the \
152 format version this crate emits",
153 );
154 assert_eq!(
155 declared, RAW_SPEC_FORMAT,
156 "Cargo.toml's raw_spec_format must match RAW_SPEC_FORMAT",
157 );
158 }
159
160 #[test]
161 fn classifies_section_names() {
162 assert_eq!(
163 classify_section(RAW_SECTION_NAME),
164 SpecSection::Raw(RAW_SPEC_FORMAT),
165 );
166 assert_eq!(
167 classify_section("thruputspecrawv99"),
168 SpecSection::Raw(99),
169 );
170 assert_eq!(classify_section(FINAL_SECTION_NAME), SpecSection::Final);
171 assert_eq!(classify_section("name"), SpecSection::Other);
172 // Unparseable suffixes are not raw sections: there is no version to
173 // report, so a decode failure is no better than ignoring them.
174 for name in [
175 "thruputspecraw",
176 "thruputspecrawvx",
177 "thruputspecrawv1x",
178 "thruputspecrawv-1",
179 ] {
180 assert_eq!(classify_section(name), SpecSection::Other, "{name}");
181 }
182 }
183}