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 "grpc.timeout",
112 "internal.listen",
113 "mempool.removal.reason",
114 "network_monitor.listen",
115 "node.role",
116 "note.execution_cycles",
117 "ntx_builder.endpoint",
118 "ntx_builder.idle_timeout",
119 "ntx_builder.listen",
120 "operation.name",
121 "path",
122 "pow.challenge.prefix",
123 "prover",
124 "prover.kind",
125 "prover.timeout",
126 "request.kind",
127 "rpc.endpoint",
128 "rpc.listen",
129 "rpc.timeout",
130 "sequencer.endpoint",
131 "service.name",
132 "service.version",
133 "shutdown.signal",
134 "sync.block_source.endpoint",
135 "task.name",
136 "transaction.id",
137 "transaction.input_notes",
138 "transaction.output_notes",
139 "tx_prover.endpoint",
140 "tx_prover.timeout",
141 "validator.admin_listen",
142 "validator.endpoints",
143 "validator.listen",
144 "validator.signer",
145 "worker.name",
146];
147
148pub trait RecordAttribute {
156 const FIELD_NAMES: &'static [&'static str];
158
159 const PLURALIZE_FIELD_NAMES: bool = false;
161
162 fn record_attribute(&self) -> impl Value + '_;
164}
165
166#[doc(hidden)]
171pub const fn field_name_allowed(field_names: &[&str], field_name: &str, pluralize: bool) -> bool {
172 let mut index = 0;
173 while index < field_names.len() {
174 let allowed = if pluralize {
175 str_eq_with_s_suffix(field_names[index], field_name)
176 } else {
177 str_eq(field_names[index], field_name)
178 };
179 if allowed {
180 return true;
181 }
182 index += 1;
183 }
184 false
185}
186
187const fn str_eq_with_s_suffix(singular: &str, plural: &str) -> bool {
188 let singular = singular.as_bytes();
189 let plural = plural.as_bytes();
190 if plural.len() != singular.len() + 1 || plural[singular.len()] != b's' {
191 return false;
192 }
193
194 let mut index = 0;
195 while index < singular.len() {
196 if singular[index] != plural[index] {
197 return false;
198 }
199 index += 1;
200 }
201 true
202}
203
204const fn str_eq(left: &str, right: &str) -> bool {
205 let left = left.as_bytes();
206 let right = right.as_bytes();
207 if left.len() != right.len() {
208 return false;
209 }
210
211 let mut index = 0;
212 while index < left.len() {
213 if left[index] != right[index] {
214 return false;
215 }
216 index += 1;
217 }
218 true
219}
220
221#[doc(hidden)]
226pub fn record_attribute<T: RecordAttribute + ?Sized>(value: &T) -> impl Value + '_ {
227 value.record_attribute()
228}
229
230macro_rules! impl_scalar_attribute {
231 ($field_names:expr; $($ty:ty),* $(,)?) => {
232 $(
233 impl RecordAttribute for $ty {
234 const FIELD_NAMES: &'static [&'static str] = $field_names;
235
236 fn record_attribute(&self) -> impl Value + '_ {
237 *self
238 }
239 }
240 )*
241 };
242}
243
244impl_scalar_attribute!(BOOLEAN_FIELD_NAMES; bool);
245impl_scalar_attribute!(
246 NUMBER_FIELD_NAMES;
247 f32,
248 f64,
249 i8,
250 i16,
251 i32,
252 i64,
253 i128,
254 isize,
255 u8,
256 u16,
257 u64,
258 u128,
259 usize,
260);
261impl_scalar_attribute!(NUMBER_FIELD_NAMES; u32);
262
263impl RecordAttribute for str {
264 const FIELD_NAMES: &'static [&'static str] = STRING_FIELD_NAMES;
265
266 fn record_attribute(&self) -> impl Value + '_ {
267 self
268 }
269}
270
271impl RecordAttribute for String {
272 const FIELD_NAMES: &'static [&'static str] = <str as RecordAttribute>::FIELD_NAMES;
273
274 fn record_attribute(&self) -> impl Value + '_ {
275 self.as_str()
276 }
277}
278
279impl<T: RecordAttribute + ?Sized> RecordAttribute for &T {
280 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
281 const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
282
283 fn record_attribute(&self) -> impl Value + '_ {
284 (*self).record_attribute()
285 }
286}
287
288impl<T: RecordAttribute> RecordAttribute for Option<T> {
289 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
290 const PLURALIZE_FIELD_NAMES: bool = T::PLURALIZE_FIELD_NAMES;
291
292 fn record_attribute(&self) -> impl Value + '_ {
293 self.as_ref().map(RecordAttribute::record_attribute)
294 }
295}
296
297impl RecordAttribute for Path {
298 const FIELD_NAMES: &'static [&'static str] = &["data.directory", "genesis.file", "path"];
299
300 fn record_attribute(&self) -> impl Value + '_ {
301 tracing::field::display(self.display())
302 }
303}
304
305impl RecordAttribute for PathBuf {
306 const FIELD_NAMES: &'static [&'static str] = <Path as RecordAttribute>::FIELD_NAMES;
307
308 fn record_attribute(&self) -> impl Value + '_ {
309 self.as_path().record_attribute()
310 }
311}
312
313impl RecordAttribute for BlockNumber {
314 const FIELD_NAMES: &'static [&'static str] = &[
315 "batch.expiration_height",
316 "batch.expires_at",
317 "batch.reference_block.number",
318 "block.from",
319 "block.number",
320 "block_range.from",
321 "block_range.to",
322 "cutoff_block",
323 "current_client_block_height",
324 "reference_block.number",
325 "snapshot.block_num",
326 "sync.upstream_block",
327 "tip.number",
328 "transaction.expires_at",
329 "transaction.reference_block.number",
330 "transaction.submitted_at",
331 ];
332
333 fn record_attribute(&self) -> impl Value + '_ {
334 self.as_u64()
335 }
336}
337
338macro_rules! impl_display_attribute {
339 ($ty:ty, $field_names:expr $(,)?) => {
340 impl RecordAttribute for $ty {
341 const FIELD_NAMES: &'static [&'static str] = $field_names;
342
343 fn record_attribute(&self) -> impl Value + '_ {
344 tracing::field::display(self)
345 }
346 }
347 };
348}
349
350impl_display_attribute!(
351 AccountId,
352 &[
353 "account.id",
354 "counter.account.id.new",
355 "counter.account.id.old",
356 "note.sender",
357 "wallet.account.id.new",
358 "wallet.account.id.old",
359 ],
360);
361impl_display_attribute!(AccountIdPrefix, &["account.id.network_prefix"]);
362impl_display_attribute!(StorageMapKey, &["account.storage.map.key"]);
363impl_display_attribute!(StorageSlotName, &["account.storage.slot"]);
364impl_display_attribute!(BatchId, &["batch.id", "block.batch.id"]);
365impl_display_attribute!(NoteId, &["note.id"]);
366impl_display_attribute!(Nullifier, &["note.nullifier"]);
367impl_display_attribute!(TransactionId, &["block.transaction.id", "transaction.id"]);
368impl_display_attribute!(
369 Word,
370 &[
371 "account.final_state.commitment",
372 "account.initial_state.commitment",
373 "account.storage.value",
374 "batch.reference_block.commitment",
375 "block.commitment",
376 "block.commitments.account",
377 "block.commitments.chain",
378 "block.commitments.kernel",
379 "block.commitments.note",
380 "block.commitments.nullifier",
381 "block.commitments.transaction",
382 "block.prev_block_commitment",
383 "block.sub_commitment",
384 "genesis.commitment",
385 "script.root",
386 "transaction.reference_block.commitment",
387 ],
388);
389
390struct AttributeList<'a, T>(&'a [T]);
395
396impl<T: Display> Display for AttributeList<'_, T> {
397 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
398 let mut values = self.0.iter();
399 let Some(first) = values.next() else {
400 return f.write_str("None");
401 };
402
403 write!(f, "[{first}")?;
404 for value in values {
405 write!(f, ", {value}")?;
406 }
407 f.write_str("]")
408 }
409}
410
411impl<T: Display + RecordAttribute> RecordAttribute for [T] {
412 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
413 const PLURALIZE_FIELD_NAMES: bool = true;
414
415 fn record_attribute(&self) -> impl Value + '_ {
416 tracing::field::display(AttributeList(self))
417 }
418}
419
420impl<T: Display + RecordAttribute, const N: usize> RecordAttribute for [T; N] {
421 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
422 const PLURALIZE_FIELD_NAMES: bool = true;
423
424 fn record_attribute(&self) -> impl Value + '_ {
425 self.as_slice().record_attribute()
426 }
427}
428
429impl<T: Display + RecordAttribute> RecordAttribute for Vec<T> {
430 const FIELD_NAMES: &'static [&'static str] = T::FIELD_NAMES;
431 const PLURALIZE_FIELD_NAMES: bool = true;
432
433 fn record_attribute(&self) -> impl Value + '_ {
434 self.as_slice().record_attribute()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use miden_protocol::account::AccountId;
441
442 use super::{AttributeList, RecordAttribute, field_name_allowed};
443
444 #[test]
445 fn lists_use_the_canonical_format() {
446 assert_eq!(AttributeList::<u32>(&[]).to_string(), "None");
447 assert_eq!(AttributeList(&[1, 2, 3]).to_string(), "[1, 2, 3]");
448 }
449
450 #[test]
451 fn references_are_approved_when_the_referenced_type_is_approved() {
452 fn assert_record_attribute(_: &impl RecordAttribute) {}
453
454 let value = "attribute";
455 assert_record_attribute(&value);
456 assert_record_attribute(&&value);
457 assert_record_attribute(&Some(value));
458 assert_record_attribute(&None::<&str>);
459 }
460
461 #[test]
462 fn field_names_are_specific_to_the_attribute_type() {
463 assert!(field_name_allowed(
464 AccountId::FIELD_NAMES,
465 "account.id",
466 AccountId::PLURALIZE_FIELD_NAMES,
467 ));
468 assert!(!field_name_allowed(
469 AccountId::FIELD_NAMES,
470 "account.ids",
471 AccountId::PLURALIZE_FIELD_NAMES,
472 ));
473 assert!(field_name_allowed(
474 <[AccountId]>::FIELD_NAMES,
475 "account.ids",
476 <[AccountId]>::PLURALIZE_FIELD_NAMES,
477 ));
478 assert!(!field_name_allowed(
479 <[AccountId]>::FIELD_NAMES,
480 "account.id",
481 <[AccountId]>::PLURALIZE_FIELD_NAMES,
482 ));
483 }
484}