Skip to main content

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}