Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cli/src/commands/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ fn free_bytes(path: &Path) -> Option<u64> {
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
}
Expand Down
135 changes: 111 additions & 24 deletions crates/cli/src/output/auth_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand All @@ -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::*;
Expand All @@ -253,6 +305,7 @@ mod tests {
total_cpu_instructions: 1500000,
total_memory_bytes: 50000,
is_error: false,
auth_signatures: vec![],
};

let trace = ExecutionTrace {
Expand Down Expand Up @@ -283,6 +336,7 @@ mod tests {
total_cpu_instructions: 800000,
total_memory_bytes: 25000,
is_error: false,
auth_signatures: vec![],
};

let main_invocation = ContractInvocation {
Expand All @@ -295,6 +349,7 @@ mod tests {
total_cpu_instructions: 2000000,
total_memory_bytes: 75000,
is_error: false,
auth_signatures: vec![],
};

let trace = ExecutionTrace {
Expand All @@ -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));
}
}
1 change: 1 addition & 0 deletions crates/cli/src/output/trace_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/replay/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn build_trace_tree(result: &SandboxResult) -> PrismResult<Vec<ContractInvoc
total_cpu_instructions: 0,
total_memory_bytes: 0,
is_error: false,
auth_signatures: Vec::new(),
};
stack.push(invocation);
}
Expand Down
3 changes: 3 additions & 0 deletions crates/core/src/types/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub struct ContractInvocation {
pub total_memory_bytes: u64,

pub is_error: bool,

#[serde(default)]
pub auth_signatures: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
107 changes: 103 additions & 4 deletions crates/core/src/xdr/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String> {
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)
Expand Down Expand Up @@ -191,8 +237,11 @@ fn hex_decode(input: &str) -> Result<Vec<u8>, 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 {
Expand All @@ -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();
Expand Down Expand Up @@ -306,4 +376,33 @@ mod tests {
let decoded = <ScVec as crate::xdr::codec::XdrCodec>::from_xdr_base64(&b64).expect("decode");
assert_eq!(scvec, decoded);
}

#[test]
fn decode_auth_entry_signature_renders_ed25519_bytes_as_hex() {
let signature: Vec<u8> = (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::<String>();

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::<u8>::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)"]);
}
}