Skip to main content

thruput_contract_util/
lib.rs

1//! Naming and hashing rules shared by `thruput-macros` and host tooling:
2//! entrypoint and interface IDs, exception selectors, event signatures and
3//! export names, and contract version handling ([`semver`]).
4//!
5//! [`remote`] holds the runtime side of the off-chain proxies that
6//! `thruput-sdk`'s `remote` feature generates.
7
8use sha2::Digest;
9use std::fmt::{self, Display};
10use thruput_xdr::contract::ContractEntrypointID;
11
12pub mod remote;
13pub mod semver;
14
15/// Standard signature for a contract method.
16///
17/// Only the method name is part of the signature today. Argument and return
18/// types may be added in a future version.
19#[derive(Debug)]
20pub struct NormalizedMethodSignature {
21    method_name: String,
22}
23
24impl NormalizedMethodSignature {
25    #[cfg(feature = "syn")]
26    pub fn new_from_signature(method: &syn::Signature) -> Self {
27        Self {
28            method_name: method.ident.to_string(),
29        }
30    }
31
32    pub fn new(method_name: &str) -> Self {
33        Self {
34            method_name: method_name.to_string(),
35        }
36    }
37
38    pub fn get_signature_string(&self) -> String {
39        self.method_name.clone()
40    }
41}
42
43impl Display for NormalizedMethodSignature {
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        f.write_str(self.method_name.as_str())
46    }
47}
48
49#[derive(Debug)]
50pub struct EntrypointSignature {
51    pub interface_name: Option<String>,
52    pub version_str: String,
53    pub method_sig: NormalizedMethodSignature,
54}
55
56impl EntrypointSignature {
57    #[cfg(feature = "syn")]
58    pub fn new(
59        interface_name: Option<&str>,
60        version_str: &str,
61        method: &syn::Signature,
62    ) -> Self {
63        Self {
64            interface_name: interface_name.map(|s| s.to_string()),
65            version_str: version_str.to_string(),
66            method_sig: NormalizedMethodSignature::new_from_signature(method),
67        }
68    }
69
70    pub fn id(&self) -> ContractEntrypointID {
71        contract_entrypoint_id(
72            self.interface_name.as_deref(),
73            self.version_str.as_str(),
74            &self.method_sig,
75        )
76    }
77}
78
79impl Display for EntrypointSignature {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        write!(
82            f,
83            "{}<{}>::{}",
84            self.interface_name
85                .as_deref()
86                .unwrap_or("_unnamed_interface"),
87            self.version_str,
88            self.method_sig,
89        )
90    }
91}
92
93/// The interface name used when a free function has no explicit
94/// `interface = "..."` override.
95pub const UNNAMED_INTERFACE: &str = "_unnamed_interface";
96
97/// The normalized (interface, MAJOR version, method-signature) triple that
98/// defines entrypoint identity. Single source of truth for
99/// [`contract_entrypoint_id`] and [`entrypoint_link_name`]: both consume this
100/// triple, so link-name equality coincides with ID equality by construction.
101fn entrypoint_id_preimage<'a>(
102    interface_name: Option<&'a str>,
103    version_str: &str,
104    method: &NormalizedMethodSignature,
105) -> (&'a str, String, String) {
106    (
107        interface_name.unwrap_or(UNNAMED_INTERFACE),
108        semver::major_version_from_string(version_str),
109        method.get_signature_string(),
110    )
111}
112
113/// Derive a standard entrypoint ID for a contract method.
114/// The interface name is a domain separator.
115///
116/// In the SDK macros (`#[contract]`):
117/// - trait and trait-impl methods use the effective interface name
118///   (`interface = "..."` override or the trait name)
119/// - bare impl methods use the effective interface name
120///   (`interface = "..."` override or the self type name)
121/// - free functions default to [`UNNAMED_INTERFACE`] unless an explicit
122///   `interface = "..."` override is present
123pub fn contract_entrypoint_id(
124    interface_name: Option<&str>,
125    version_str: &str,
126    method: &NormalizedMethodSignature,
127) -> ContractEntrypointID {
128    let (interface_name, major_version, method_sig) =
129        entrypoint_id_preimage(interface_name, version_str, method);
130
131    // Entrypoint ID = sha256(INTERFACE_NAME :: MAJOR_VERSION :: METHOD_SIGNATURE)[0..8]
132    //
133    // The `::` separators are required for the same reason as in
134    // `exception_selector`/`event_signature`: without them, distinct field
135    // triples can concatenate to identical bytes (e.g. interface "Foo1" +
136    // major "2" + method "x" vs "Foo" + "12" + "x", both "Foo12x"), colliding
137    // to the same id and silently misrouting a call.
138    let mut hasher = sha2::Sha256::new();
139    hasher.update(interface_name.as_bytes());
140    hasher.update(b"::");
141    hasher.update(major_version.as_bytes());
142    hasher.update(b"::");
143    hasher.update(method_sig.as_bytes());
144    let result = hasher.finalize();
145
146    let entrypoint_id: [u8; 8] = result[0..8].try_into().unwrap();
147    ContractEntrypointID::from(entrypoint_id)
148}
149
150/// Derive the interface ID of an (interface name, version) pair: the
151/// identity of an interface at a major version, shared by every entrypoint
152/// the interface exports.
153///
154/// Truncated to 8 bytes exactly like [`contract_entrypoint_id`], and for the
155/// same reason: the ID is a *name*, derived from `(interface, MAJOR)` that
156/// anyone may declare, so its width only has to keep honestly distinct
157/// interfaces apart, not resist an adversary. The preimage carries its own
158/// domain tag so an interface ID never collides with an entrypoint ID
159/// preimage. `#[contract]` emits it as `Trait::INTERFACE_ID` and
160/// `<Iface>Entrypoints::INTERFACE_ID`; `thruput-erc165-if` is the on-chain
161/// query surface for it.
162pub fn interface_id(
163    interface_name: Option<&str>,
164    version_str: &str,
165) -> [u8; 8] {
166    let interface_name = interface_name.unwrap_or(UNNAMED_INTERFACE);
167    let major_version = semver::major_version_from_string(version_str);
168
169    // Interface ID = sha256("thruput-interface" :: INTERFACE_NAME :: MAJOR_VERSION)[0..8]
170    let mut hasher = sha2::Sha256::new();
171    hasher.update(b"thruput-interface");
172    hasher.update(b"::");
173    hasher.update(interface_name.as_bytes());
174    hasher.update(b"::");
175    hasher.update(major_version.as_bytes());
176    let result = hasher.finalize();
177
178    result[0..8].try_into().unwrap()
179}
180
181/// The export symbol name of the entrypoint for the given
182/// (interface, version, method) triple: [`contract_entrypoint_id`] rendered
183/// through [`entrypoint_export_name`]. Use this instead of composing the two
184/// by hand so that every caller stays in sync if the ID preimage ever
185/// changes (for example, if a signature checksum is added to it).
186pub fn entrypoint_export_name_for(
187    interface_name: Option<&str>,
188    version_str: &str,
189    method: &NormalizedMethodSignature,
190) -> String {
191    entrypoint_export_name(contract_entrypoint_id(
192        interface_name,
193        version_str,
194        method,
195    ))
196}
197
198pub fn entrypoint_export_name(id: ContractEntrypointID) -> String {
199    format!("{:016X}", u64::from_be_bytes(*id))
200}
201
202/// Human-readable symbol name carrying the entrypoint-ID *preimage*, used as
203/// the rv32 wrapper's `export_name` so a duplicate entrypoint ID fails the
204/// build with an error naming the interface and method (rather than a bare
205/// 16-hex ID). Built from the same (interface, MAJOR version, method
206/// signature) triple as [`contract_entrypoint_id`], so link-name equality
207/// coincides exactly with entrypoint-ID equality. Symbol names never reach
208/// the lowered rv32 image (they live in non-allocatable ELF sections), so
209/// readability costs nothing in the deployed artifact.
210pub fn entrypoint_link_name(
211    interface_name: Option<&str>,
212    version_str: &str,
213    method: &NormalizedMethodSignature,
214) -> String {
215    let (interface_name, major_version, method_sig) =
216        entrypoint_id_preimage(interface_name, version_str, method);
217    format!(
218        "__thruput_entrypoint::{interface_name}::{major_version}::{method_sig}"
219    )
220}
221
222/// Derive a standard exception selector for a contract exception type.
223///
224/// Exception selector = sha256(MAJOR_VERSION :: CRATE_NAME :: TYPE_NAME)[0..8]
225///
226/// The crate name is included to reduce collisions between types with the
227/// same name defined in different crates. Correctness across breaking
228/// changes is delegated to SemVer — if the exception type changes in a
229/// wire-incompatible way, the crate's major version must bump, which
230/// changes the selector.
231///
232/// Note: structurally different types with the same name, crate, and major
233/// version will intentionally share a selector. This is a tradeoff of
234/// stability over collision resistance, acceptable given the u64 space.
235pub fn exception_selector(
236    version_str: &str,
237    crate_name: &str,
238    type_name: &str,
239) -> u64 {
240    let major_version = semver::major_version_from_string(version_str);
241    exception_selector_from_major(&major_version, crate_name, type_name)
242}
243
244/// [`exception_selector`] with the major version already derived (the exact
245/// preimage component, e.g. `"1"` or `"0.9"` — not a full semver string).
246///
247/// Use this when checking a recorded (major_version, crate_name, type_name)
248/// preimage against a baked selector.
249pub fn exception_selector_from_major(
250    major_version: &str,
251    crate_name: &str,
252    type_name: &str,
253) -> u64 {
254    let mut hasher = sha2::Sha256::new();
255    hasher.update(major_version.as_bytes());
256    hasher.update(b"::");
257    hasher.update(crate_name.as_bytes());
258    hasher.update(b"::");
259    hasher.update(type_name.as_bytes());
260    let result = hasher.finalize();
261
262    u64::from_be_bytes(result[..8].try_into().unwrap())
263}
264
265/// Derive a standard event signature for a contract event type.
266///
267/// Event signature = sha256(MAJOR_VERSION :: CRATE_NAME :: TYPE_NAME)
268///
269/// Uses the same hashing scheme as `exception_selector` but returns the
270/// full 32-byte hash instead of truncating to u64.
271pub fn event_signature(
272    version_str: &str,
273    crate_name: &str,
274    type_name: &str,
275) -> [u8; 32] {
276    let major_version = semver::major_version_from_string(version_str);
277
278    let mut hasher = sha2::Sha256::new();
279    hasher.update(major_version.as_bytes());
280    hasher.update(b"::");
281    hasher.update(crate_name.as_bytes());
282    hasher.update(b"::");
283    hasher.update(type_name.as_bytes());
284    hasher.finalize().into()
285}
286
287#[cfg(test)]
288mod tests;