From ece7370bd29045422c74bc79bc3b19f8fe00922b Mon Sep 17 00:00:00 2001 From: William Isode Date: Wed, 17 Jun 2026 01:26:45 +0800 Subject: [PATCH] Decode auth signature bytes for reports --- crates/cli/src/commands/diagnostic.rs | 2 +- crates/cli/src/output/auth_tree.rs | 135 +++++++++++++++++++++----- crates/cli/src/output/trace_tree.rs | 1 + crates/core/src/replay/trace.rs | 1 + crates/core/src/types/trace.rs | 3 + crates/core/src/xdr/codec.rs | 107 +++++++++++++++++++- 6 files changed, 220 insertions(+), 29 deletions(-) diff --git a/crates/cli/src/commands/diagnostic.rs b/crates/cli/src/commands/diagnostic.rs index 499a7f21..72617a6c 100644 --- a/crates/cli/src/commands/diagnostic.rs +++ b/crates/cli/src/commands/diagnostic.rs @@ -227,7 +227,7 @@ fn free_bytes(path: &Path) -> Option { let mut stat: libc::statvfs = unsafe { std::mem::zeroed() }; let rc = unsafe { libc::statvfs(cpath.as_ptr(), &mut stat) }; if rc == 0 { - Some(stat.f_bavail * stat.f_frsize) + Some((stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64)) } else { None } diff --git a/crates/cli/src/output/auth_tree.rs b/crates/cli/src/output/auth_tree.rs index f6c25b58..85932825 100644 --- a/crates/cli/src/output/auth_tree.rs +++ b/crates/cli/src/output/auth_tree.rs @@ -101,18 +101,7 @@ fn render_invocation( auth_prefix, icons::AUTH_REQUIRED )?; - writeln!( - output, - "{}│ └─ {} Provided: ✓", - auth_prefix, - icons::AUTH_PROVIDED - )?; - writeln!( - output, - "{}│ └─ {} Verified: ✓", - auth_prefix, - icons::AUTH_PROVIDED - )?; + render_auth_status(output, &auth_prefix, &invocation.auth_signatures)?; writeln!(output, "{auth_prefix}├─ ⚡ Resources:")?; writeln!( @@ -197,18 +186,7 @@ fn render_auth_invocation( auth_prefix, icons::AUTH_REQUIRED )?; - writeln!( - output, - "{}│ └─ {} Contract authority", - auth_prefix, - icons::AUTH_PROVIDED - )?; - writeln!( - output, - "{}│ └─ {} Function caller", - auth_prefix, - icons::AUTH_PROVIDED - )?; + render_required_signatures(output, &auth_prefix, &invocation.auth_signatures)?; if invocation.sub_invocations.is_empty() { writeln!( @@ -232,6 +210,80 @@ fn render_auth_invocation( Ok(()) } +fn render_auth_status( + output: &mut String, + auth_prefix: &str, + signatures: &[String], +) -> anyhow::Result<()> { + writeln!( + output, + "{}│ ├─ {} Provided: ✓", + auth_prefix, + icons::AUTH_PROVIDED + )?; + writeln!( + output, + "{}│ ├─ {} Verified: ✓", + auth_prefix, + icons::AUTH_PROVIDED + )?; + + if signatures.is_empty() { + writeln!(output, "{auth_prefix}│ └─ No decoded signatures")?; + } else { + writeln!(output, "{auth_prefix}│ └─ Decoded signatures:")?; + render_signature_values(output, auth_prefix, signatures)?; + } + + Ok(()) +} + +fn render_required_signatures( + output: &mut String, + auth_prefix: &str, + signatures: &[String], +) -> anyhow::Result<()> { + if signatures.is_empty() { + writeln!( + output, + "{}│ ├─ {} Contract authority", + auth_prefix, + icons::AUTH_PROVIDED + )?; + writeln!( + output, + "{}│ └─ {} Function caller", + auth_prefix, + icons::AUTH_PROVIDED + )?; + } else { + render_signature_values(output, auth_prefix, signatures)?; + } + + Ok(()) +} + +fn render_signature_values( + output: &mut String, + auth_prefix: &str, + signatures: &[String], +) -> anyhow::Result<()> { + for (i, signature) in signatures.iter().enumerate() { + let connector = if i == signatures.len().saturating_sub(1) { + box_chars::TOP_RIGHT + } else { + box_chars::VERTICAL_RIGHT + }; + writeln!( + output, + "{auth_prefix}│ {connector} {} Ed25519 signature: {signature}", + icons::AUTH_PROVIDED + )?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -253,6 +305,7 @@ mod tests { total_cpu_instructions: 1500000, total_memory_bytes: 50000, is_error: false, + auth_signatures: vec![], }; let trace = ExecutionTrace { @@ -283,6 +336,7 @@ mod tests { total_cpu_instructions: 800000, total_memory_bytes: 25000, is_error: false, + auth_signatures: vec![], }; let main_invocation = ContractInvocation { @@ -295,6 +349,7 @@ mod tests { total_cpu_instructions: 2000000, total_memory_bytes: 75000, is_error: false, + auth_signatures: vec![], }; let trace = ExecutionTrace { @@ -312,4 +367,36 @@ mod tests { assert!(result.contains("approve")); assert!(result.contains("🔗")); } + + #[test] + fn test_render_auth_only_shows_decoded_signature_hex() { + let signature = "00".repeat(64); + let invocation = ContractInvocation { + contract_id: "SIGNED123...".to_string(), + function_name: "transfer".to_string(), + arguments: vec![], + return_value: Some("Success".to_string()), + host_calls: vec![], + sub_invocations: vec![], + total_cpu_instructions: 1000, + total_memory_bytes: 512, + is_error: false, + auth_signatures: vec![signature.clone()], + }; + + let trace = ExecutionTrace { + tx_hash: "signed789...".to_string(), + ledger_sequence: 789, + network: "testnet".to_string(), + invocations: vec![invocation], + state_diff: Default::default(), + resource_profile: Default::default(), + diagnostic_events: vec![], + }; + + let result = render_auth_only(&trace).unwrap(); + + assert!(result.contains("Ed25519 signature")); + assert!(result.contains(&signature)); + } } diff --git a/crates/cli/src/output/trace_tree.rs b/crates/cli/src/output/trace_tree.rs index 88534b88..e5ccc06d 100644 --- a/crates/cli/src/output/trace_tree.rs +++ b/crates/cli/src/output/trace_tree.rs @@ -356,6 +356,7 @@ mod tests { total_cpu_instructions: 1500, total_memory_bytes: 2048, is_error: false, + auth_signatures: vec![], }; let mut buffer = Buffer::no_color(); diff --git a/crates/core/src/replay/trace.rs b/crates/core/src/replay/trace.rs index 7b3bb9cb..c43f53fd 100644 --- a/crates/core/src/replay/trace.rs +++ b/crates/core/src/replay/trace.rs @@ -31,6 +31,7 @@ pub fn build_trace_tree(result: &SandboxResult) -> PrismResult, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/core/src/xdr/codec.rs b/crates/core/src/xdr/codec.rs index f0d6cf2e..2e960531 100644 --- a/crates/core/src/xdr/codec.rs +++ b/crates/core/src/xdr/codec.rs @@ -3,8 +3,8 @@ use crate::error::{PrismError, PrismResult}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use stellar_xdr::curr::{ - DiagnosticEvent, LedgerEntry, Limits, ReadXdr, ScVec, TransactionEnvelope, TransactionMeta, - WriteXdr, TransactionResult, + DiagnosticEvent, LedgerEntry, Limits, ReadXdr, ScVal, ScVec, SorobanAuthorizationEntry, + SorobanCredentials, TransactionEnvelope, TransactionMeta, TransactionResult, WriteXdr, }; pub trait XdrCodec: Sized { @@ -156,6 +156,52 @@ pub fn encode_xdr_base64(bytes: &[u8]) -> String { STANDARD.encode(bytes) } +/// Decode signatures from Soroban auth-entry XDR into display-ready labels. +pub fn decode_auth_entry_signatures(auth_entries_xdr: &[String]) -> Vec { + auth_entries_xdr + .iter() + .map(|entry_xdr| decode_auth_entry_signature(entry_xdr)) + .collect() +} + +fn decode_auth_entry_signature(entry_xdr_base64: &str) -> String { + let entry = match decode_xdr_base64(entry_xdr_base64).and_then(|bytes| { + SorobanAuthorizationEntry::from_xdr(&bytes, Limits::none()).map_err(|e| { + PrismError::XdrDecodingFailed { + type_name: "SorobanAuthorizationEntry", + reason: e.to_string(), + } + }) + }) { + Ok(entry) => entry, + Err(e) => return format!("malformed_signature(auth_entry_xdr: {e})"), + }; + + match &entry.credentials { + SorobanCredentials::Address(credentials) => decode_signature_scval(&credentials.signature), + SorobanCredentials::SourceAccount => "source_account_signature".to_string(), + } +} + +fn decode_signature_scval(signature: &ScVal) -> String { + match signature { + ScVal::Bytes(bytes) => decode_raw_signature_bytes(bytes.as_ref()), + other => format!("malformed_signature(non_bytes_scval: {})", other.name()), + } +} + +fn decode_raw_signature_bytes(bytes: &[u8]) -> String { + match bytes.len() { + 0 => "malformed_signature(empty)".to_string(), + 64 => hex_encode(bytes), + len => format!("malformed_signature({len}_bytes)"), + } +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + /// Decode a transaction hash from hex string. pub fn decode_tx_hash(hash_hex: &str) -> PrismResult<[u8; 32]> { let bytes = hex_decode(hash_hex) @@ -191,8 +237,11 @@ fn hex_decode(input: &str) -> Result, String> { mod tests { use super::*; use stellar_xdr::curr::{ - ExtensionPoint, Memo, MuxedAccount, OperationMeta, Preconditions, SequenceNumber, - Transaction, TransactionExt, TransactionMetaV3, TransactionV1Envelope, Uint256, + AccountId, ExtensionPoint, InvokeContractArgs, Memo, MuxedAccount, OperationMeta, + Preconditions, ScAddress, ScBytes, ScSymbol, ScVal, SequenceNumber, + SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction, + SorobanAuthorizedInvocation, SorobanCredentials, Transaction, TransactionExt, + TransactionMetaV3, TransactionV1Envelope, Uint256, }; fn make_test_envelope() -> TransactionEnvelope { @@ -210,6 +259,27 @@ mod tests { }) } + fn make_auth_entry(signature: ScVal) -> SorobanAuthorizationEntry { + SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: ScAddress::Account(AccountId( + stellar_xdr::curr::PublicKey::PublicKeyTypeEd25519(Uint256([7; 32])), + )), + nonce: 123, + signature_expiration_ledger: 456, + signature, + }), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: ScAddress::Contract(stellar_xdr::curr::Hash([8; 32])), + function_name: ScSymbol::try_from("transfer".as_bytes().to_vec()).unwrap(), + args: vec![].try_into().unwrap(), + }), + sub_invocations: vec![].try_into().unwrap(), + }, + } + } + #[test] fn test_xdr_codec_round_trip() { let envelope = make_test_envelope(); @@ -306,4 +376,33 @@ mod tests { let decoded = ::from_xdr_base64(&b64).expect("decode"); assert_eq!(scvec, decoded); } + + #[test] + fn decode_auth_entry_signature_renders_ed25519_bytes_as_hex() { + let signature: Vec = (0..64).collect(); + let entry = make_auth_entry(ScVal::Bytes(ScBytes::try_from(signature.clone()).unwrap())); + let entry_xdr = entry + .to_xdr_base64(Limits::none()) + .expect("encode auth entry"); + + let decoded = decode_auth_entry_signatures(&[entry_xdr]); + let expected = signature + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + + assert_eq!(decoded, vec![expected]); + } + + #[test] + fn decode_auth_entry_signature_labels_empty_signature_as_malformed() { + let entry = make_auth_entry(ScVal::Bytes(ScBytes::try_from(Vec::::new()).unwrap())); + let entry_xdr = entry + .to_xdr_base64(Limits::none()) + .expect("encode empty auth entry"); + + let decoded = decode_auth_entry_signatures(&[entry_xdr]); + + assert_eq!(decoded, vec!["malformed_signature(empty)"]); + } }