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