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;