@@ -2538,6 +2538,24 @@ async fn execute_single_fetch(
25382538// Audit result handler
25392539// ---------------------------------------------------------------------------
25402540
2541+ /// Format the first confirmed-failed key as a 16-hex-char label.
2542+ ///
2543+ /// Pairs with `challenged_peer` to form a stable cross-host correlation
2544+ /// handle in the audit-failure log line, e.g.
2545+ ///
2546+ /// ```text
2547+ /// Audit failure for <peer>: …, `first_failed_key=0x18878f1d2d9e0612`
2548+ /// ```
2549+ ///
2550+ /// Falls back to `"0x"` when the list is empty so the log line never
2551+ /// contains a misleading default.
2552+ fn first_failed_key_label ( confirmed_failed_keys : & [ XorName ] ) -> String {
2553+ confirmed_failed_keys. first ( ) . map_or_else (
2554+ || "0x" . to_string ( ) ,
2555+ |k| format ! ( "0x{}" , hex:: encode( & k[ ..8 ] ) ) ,
2556+ )
2557+ }
2558+
25412559/// Handle audit result: log findings and emit trust events.
25422560async fn handle_audit_result (
25432561 result : & AuditTickResult ,
@@ -2573,8 +2591,9 @@ async fn handle_audit_result(
25732591 ..
25742592 } = evidence
25752593 {
2594+ let first_failed_key = first_failed_key_label ( confirmed_failed_keys) ;
25762595 error ! (
2577- "Audit failure for {challenged_peer}: reason={reason:?}, confirmed_failed_keys={}, challenged_keys={}, absent_keys={}, digest_mismatch_keys={}" ,
2596+ "Audit failure for {challenged_peer}: reason={reason:?}, confirmed_failed_keys={}, challenged_keys={}, absent_keys={}, digest_mismatch_keys={}, first_failed_key={first_failed_key} " ,
25782597 confirmed_failed_keys. len( ) ,
25792598 summary. challenged_keys,
25802599 summary. absent_keys,
@@ -2650,7 +2669,7 @@ fn audit_failure_clears_bootstrap_claim(reason: &AuditFailureReason) -> bool {
26502669#[ cfg( test) ]
26512670#[ allow( clippy:: unwrap_used, clippy:: expect_used, clippy:: panic) ]
26522671mod tests {
2653- use super :: audit_failure_clears_bootstrap_claim;
2672+ use super :: { audit_failure_clears_bootstrap_claim, first_failed_key_label } ;
26542673 use crate :: replication:: types:: AuditFailureReason ;
26552674
26562675 #[ test]
@@ -2674,4 +2693,40 @@ mod tests {
26742693 ) ;
26752694 }
26762695 }
2696+
2697+ #[ test]
2698+ fn first_failed_key_label_truncates_to_16_hex_chars ( ) {
2699+ // The high-order 8 bytes of the XorName determine the label so an
2700+ // operator can group audit-failures on the same chunk prefix.
2701+ let mut key = [ 0u8 ; 32 ] ;
2702+ key[ 0 ] = 0x18 ;
2703+ key[ 7 ] = 0xff ;
2704+ // Low-order bytes (positions 8..32) are deliberately set to 0xAA
2705+ // to verify they are NOT included in the label.
2706+ for byte in & mut key[ 8 ..] {
2707+ * byte = 0xAA ;
2708+ }
2709+ let label = first_failed_key_label ( & [ key] ) ;
2710+ // Only the first 8 bytes are encoded, low-order bytes are dropped.
2711+ assert_eq ! ( label, "0x18000000000000ff" ) ;
2712+ assert_eq ! ( label. len( ) , "0x" . len( ) + 16 ) ;
2713+ }
2714+
2715+ #[ test]
2716+ fn first_failed_key_label_falls_back_when_empty ( ) {
2717+ // Should never happen in production (handle_audit_failure rejects
2718+ // empty sets), but the formatter must still produce a valid label
2719+ // so the log line doesn't contain a misleading default.
2720+ assert_eq ! ( first_failed_key_label( & [ ] ) , "0x" ) ;
2721+ }
2722+
2723+ #[ test]
2724+ fn first_failed_key_label_uses_first_key_only ( ) {
2725+ let first = [ 0x11u8 ; 32 ] ;
2726+ let second = [ 0x22u8 ; 32 ] ;
2727+ assert_eq ! (
2728+ first_failed_key_label( & [ first, second] ) ,
2729+ format!( "0x{}" , hex:: encode( & first[ ..8 ] ) )
2730+ ) ;
2731+ }
26772732}
0 commit comments