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