1use std::fmt::{self, Display, Formatter};
2use std::path::{Path, PathBuf};
3
4use miden_protocol::Word;
5use miden_protocol::account::{AccountId, AccountIdPrefix, StorageMapKey, StorageSlotName};
6use miden_protocol::batch::BatchId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{NoteId, Nullifier};
9use miden_protocol::transaction::TransactionId;
10use tracing::Value;
11
12const BOOLEAN_FIELD_NAMES: &[&str] = &[
13 "account.updated",
14 "note.erased",
15 "note.id_resolved",
16 "panic",
17 "request.include_mmr_proof",
18 "request.include_proof",
19 "rpc.authentication.configured",
20];
21
22const NUMBER_FIELD_NAMES: &[&str] = &[
23 "account.id.length",
24 "account.index",
25 "asset.amount",
26 "batch.expiration_height",
27 "batch.expires_at",
28 "batch.reference_block.number",
29 "batch.size",
30 "block.from",
31 "block.number",
32 "block.protocol.version",
33 "block.size",
34 "block.timestamp",
35 "block_range.from",
36 "block_range.to",
37 "counter.failures.consecutive",
38 "counter.latency.timeout_ms",
39 "counter.value.expected",
40 "counter.value.observed",
41 "counter.value.target",
42 "current_client_block_height",
43 "cutoff_block",
44 "db.account_state_forest.size",
45 "db.account_tree.size",
46 "db.block_store.size",
47 "db.nullifier_tree.size",
48 "db.sqlite.connection_pool_size",
49 "db.sqlite.size",
50 "db.sqlite.wal.size",
51 "dice_roll",
52 "failure_rate",
53 "inputs_size",
54 "mempool.accounts",
55 "mempool.batches.proposed",
56 "mempool.batches.proven",
57 "mempool.nullifiers",
58 "mempool.output_notes",
59 "mempool.transactions.unbatched",
60 "mempool.transactions.uncommitted",
61 "note.tag",
62 "ntx_builder.max_cycles",
63 "ntx_builder.tx_expiration_delta",
64 "port",
65 "pow.hash",
66 "pow.nonce",
67 "pow.target",
68 "pow.target.leading_zero_bits",
69 "prefix_len",
70 "proof_size",
71 "prover.capacity",
72 "prover.port",
73 "prover.proof_type.raw",
74 "reference_block.number",
75 "retry.attempt",
76 "retry.delay_ms",
77 "shutdown.grace_period_ms",
78 "snapshot.block_num",
79 "snapshot.lifetime_ms",
80 "snapshot.superseded_for_ms",
81 "snapshots.live",
82 "subscription.idle_ms",
83 "subscription.stall_timeout_ms",
84 "sync.block_gap",
85 "sync.ready_threshold",
86 "sync.upstream_block",
87 "timeout.ms",
88 "tip.number",
89 "tip.stale_duration_secs",
90 "transaction.expiration_delta",
91 "transaction.expires_at",
92 "transaction.reference_block.number",
93 "transaction.submitted_at",
94 "worker.status.raw",
95 "workers.active",
96 "workers.capacity",
97];
98
99const STRING_FIELD_NAMES: &[&str] = &[
100 "account.id",
101 "account.storage.kind",
102 "account.storage.map.entry.operation",
103 "account.storage.operation",
104 "asset.symbol",
105 "batch.interval",
106 "block.interval",
107 "dependency.endpoint",
108 "dependency.name",
109 "genesis.source",
110 "genesis.source.kind",
111 "internal.listen",
112 "mempool.removal.reason",
113 "network_monitor.listen",
114 "node.role",
115 "note.execution_cycles",
116 "ntx_builder.endpoint",
117 "ntx_builder.idle_timeout",
118 "ntx_builder.listen",
119 "operation.name",
120 "path",
121 "pow.challenge.prefix",
122 "prover",
123 "prover.kind",
124 "prover.timeout",
125 "request.kind",
126 "rpc.endpoint",
127 "rpc.listen",
128 "sequencer.endpoint",
129 "service.name",
130 "service.version",
131 "shutdown.signal",
132 "sync.block_source.endpoint",
133 "task.name",
134 "transaction.id",
135 "transaction.input_notes",
136 "transaction.output_notes",
137 "tx_prover.endpoint",
138 "validator.admin_listen",
139 "validator.endpoints",
140 "validator.listen",
141 "validator.signer",
142 "worker.name",
143];
144
145pub trait RecordAttribute {
153 const FIELD_NAMES: &'static [&'static str];
155
156 const PLURALIZE_FIELD_NAMES: bool = false;
158
159 fn record_attribute(&self) -> impl Value + '_;
161}
162
163#[doc(hidden)]
168pub const fn field_name_allowed(field_names: &[&str], field_name: &str, pluralize: bool) -> bool {
169 let mut index = 0;
170 while index < field_names.len() {
171 let allowed = if pluralize {
172 str_eq_with_s_suffix(field_names[index], field_name)
173 } else {
174 str_eq(field_names[index], field_name)
175 };
176 if allowed {
177 return true;
178 }
179 index += 1;
180 }
181 false
182}
183
184const fn str_eq_with_s_suffix(singular: &str, plural: &str) -> bool {
185 let singular = singular.as_bytes();
186 let plural = plural.as_bytes();
187 if plural.len() != singular.len() + 1 || plural[singular.len()] != b's' {
188 return false;
189 }
190
191 let mut index = 0;
192 while index < singular.len() {
193 if singular[index] != plural[index] {
194 return false;
195 }
196 index += 1;
197 }
198 true
199}
200
201const fn str_eq(left: &str, right: &str) -> bool {
202 let left = left.as_bytes();
203 let right = right.as_bytes();
204 if left.len() != right.len() {
205 return false;
206 }
207
208 let mut index = 0;
209 while index < left.len() {
210 if left[index] != right[index] {
211 return false;
212 }
213 index += 1;
214 }
215 true
216}
217
218#[doc(hidden)]
223pub fn record_attribute<T: RecordAttribute + ?Sized>(value: &T) -> impl Value + '_ {
224 value.record_attribute()
225}
226
227macro_rules! impl_scalar_attribute {
228 ($field_names:expr; $($ty:ty),* $(,)?) => {
229 $(
230 impl RecordAttribute for $ty {
231 const FIELD_NAMES: &'static [&'static str] = $field_names;
232
233 fn record_attribute(&self) -> impl Value + '_ {
234 *self
235 }
236 }
237 )*
238 };
239}
240
241impl_scalar_attribute!(BOOLEAN_FIELD_NAMES; bool);
242impl_scalar_attribute!(
243 NUMBER_FIELD_NAMES;
244 f32,
245 f64,
246 i8,
247 i16,
248 i32,
249 i64,
250 i128,
251 isize,
252 u8,
253 u16,
254 u64,
255 u128,
256 usize,
257);
258impl_scalar_attribute!(NUMBER_FIELD_NAMES; u32);
259
260impl RecordAttribute for str {
261 const FIELD_NAMES: &'static [&'static str] = STRING_FIELD_NAMES;
262
263 fn record_attribute(&self) -> impl Value + '_ {
264 self
265 }
266}
267
268impl RecordAttribute for String {
269 const FIELD_NAMES: &'static [&'static str] = <str as RecordAttribute>::FIELD_NAMES;
270
271 fn record_attribute(&self) -> impl Value + '_ {
272 self.as_str()
273 }
274}
275
276impl<T: RecordAttribute + ?Sized> RecordAttribute for &T {
277 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
278 const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
279
280 fn record_attribute(&self) -> impl Value + '_ {
281 (*self).record_attribute()
282 }
283}
284
285impl<T: RecordAttribute> RecordAttribute for Option<T> {
286 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
287 const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
288
289 fn record_attribute(&self) -> impl Value + '_ {
290 self.as_ref().map(RecordAttribute::record_attribute)
291 }
292}
293
294impl RecordAttribute for Path {
295 const FIELD_NAMES: &'static [&'static str] = &["data.directory", "genesis.file", "path"];
296
297 fn record_attribute(&self) -> impl Value + '_ {
298 tracing::field::display(self.display())
299 }
300}
301
302impl RecordAttribute for PathBuf {
303 const FIELD_NAMES: &'static [&'static str] = <Path as RecordAttribute>::FIELD_NAMES;
304
305 fn record_attribute(&self) -> impl Value + '_ {
306 self.as_path().record_attribute()
307 }
308}
309
310impl RecordAttribute for BlockNumber {
311 const FIELD_NAMES: &'static [&'static str] = &[
312 "batch.expiration_height",
313 "batch.expires_at",
314 "batch.reference_block.number",
315 "block.from",
316 "block.number",
317 "block_range.from",
318 "block_range.to",
319 "cutoff_block",
320 "current_client_block_height",
321 "reference_block.number",
322 "snapshot.block_num",
323 "sync.upstream_block",
324 "tip.number",
325 "transaction.expires_at",
326 "transaction.reference_block.number",
327 "transaction.submitted_at",
328 ];
329
330 fn record_attribute(&self) -> impl Value + '_ {
331 self.as_u64()
332 }
333}
334
335macro_rules! impl_display_attribute {
336 ($ty:ty, $field_names:expr $(,)?) => {
337 impl RecordAttribute for $ty {
338 const FIELD_NAMES: &'static [&'static str] = $field_names;
339
340 fn record_attribute(&self) -> impl Value + '_ {
341 tracing::field::display(self)
342 }
343 }
344 };
345}
346
347impl_display_attribute!(
348 AccountId,
349 &[
350 "account.id",
351 "counter.account.id.new",
352 "counter.account.id.old",
353 "note.sender",
354 "wallet.account.id.new",
355 "wallet.account.id.old",
356 ],
357);
358impl_display_attribute!(AccountIdPrefix, &["account.id.network_prefix"]);
359impl_display_attribute!(StorageMapKey, &["account.storage.map.key"]);
360impl_display_attribute!(StorageSlotName, &["account.storage.slot"]);
361impl_display_attribute!(BatchId, &["batch.id", "block.batch.id"]);
362impl_display_attribute!(NoteId, &["note.id"]);
363impl_display_attribute!(Nullifier, &["note.nullifier"]);
364impl_display_attribute!(TransactionId, &["block.transaction.id", "transaction.id"]);
365impl_display_attribute!(
366 Word,
367 &[
368 "account.final_state.commitment",
369 "account.initial_state.commitment",
370 "account.storage.value",
371 "batch.reference_block.commitment",
372 "block.commitment",
373 "block.commitments.account",
374 "block.commitments.chain",
375 "block.commitments.kernel",
376 "block.commitments.note",
377 "block.commitments.nullifier",
378 "block.commitments.transaction",
379 "block.prev_block_commitment",
380 "block.sub_commitment",
381 "genesis.commitment",
382 "script.root",
383 "transaction.reference_block.commitment",
384 ],
385);
386
387struct AttributeList<'a, T>(&'a [T]);
392
393impl<T: Display> Display for AttributeList<'_, T> {
394 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
395 let mut values = self.0.iter();
396 let Some(first) = values.next() else {
397 return f.write_str("None");
398 };
399
400 write!(f, "[{first}")?;
401 for value in values {
402 write!(f, ", {value}")?;
403 }
404 f.write_str("]")
405 }
406}
407
408impl<T: Display + RecordAttribute> RecordAttribute for [T] {
409 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
410 const PLURALIZE_FIELD_NAMES: bool = true;
411
412 fn record_attribute(&self) -> impl Value + '_ {
413 tracing::field::display(AttributeList(self))
414 }
415}
416
417impl<T: Display + RecordAttribute, const N: usize> RecordAttribute for [T; N] {
418 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
419 const PLURALIZE_FIELD_NAMES: bool = true;
420
421 fn record_attribute(&self) -> impl Value + '_ {
422 self.as_slice().record_attribute()
423 }
424}
425
426impl<T: Display + RecordAttribute> RecordAttribute for Vec<T> {
427 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
428 const PLURALIZE_FIELD_NAMES: bool = true;
429
430 fn record_attribute(&self) -> impl Value + '_ {
431 self.as_slice().record_attribute()
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use miden_protocol::account::AccountId;
438
439 use super::{AttributeList, RecordAttribute, field_name_allowed};
440
441 #[test]
442 fn lists_use_the_canonical_format() {
443 assert_eq!(AttributeList::<u32>(&[]).to_string(), "None");
444 assert_eq!(AttributeList(&[1, 2, 3]).to_string(), "[1, 2, 3]");
445 }
446
447 #[test]
448 fn references_are_approved_when_the_referenced_type_is_approved() {
449 fn assert_record_attribute(_: &impl RecordAttribute) {}
450
451 let value = "attribute";
452 assert_record_attribute(&value);
453 assert_record_attribute(&&value);
454 assert_record_attribute(&Some(value));
455 assert_record_attribute(&None::<&str>);
456 }
457
458 #[test]
459 fn field_names_are_specific_to_the_attribute_type() {
460 assert!(field_name_allowed(
461 AccountId::FIELD_NAMES,
462 "account.id",
463 AccountId::PLURALIZE_FIELD_NAMES,
464 ));
465 assert!(!field_name_allowed(
466 AccountId::FIELD_NAMES,
467 "account.ids",
468 AccountId::PLURALIZE_FIELD_NAMES,
469 ));
470 assert!(field_name_allowed(
471 <[AccountId]>::FIELD_NAMES,
472 "account.ids",
473 <[AccountId]>::PLURALIZE_FIELD_NAMES,
474 ));
475 assert!(!field_name_allowed(
476 <[AccountId]>::FIELD_NAMES,
477 "account.id",
478 <[AccountId]>::PLURALIZE_FIELD_NAMES,
479 ));
480 }
481}