use crate::cbor::{decode_cbor_permissive, PermissiveValue};
use crate::cose::ed25519_verify;
use crate::hash::blake2b256;
use crate::recipient::bech32_encode_no_limit;
use crate::verifier::types::{CardanoNetwork, VerifyTxOutput, VerifyTxSummary, VerifyTxWitness};
const ED25519_PUBLIC_KEY_LENGTH: usize = 32;
const ED25519_SIGNATURE_LENGTH: usize = 64;
const BODY_KEY_INPUTS: u64 = 0;
const BODY_KEY_OUTPUTS: u64 = 1;
const BODY_KEY_FEE: u64 = 2;
const BODY_KEY_INVALID_HEREAFTER: u64 = 3; const BODY_KEY_INVALID_BEFORE: u64 = 8; const BODY_KEY_REQUIRED_SIGNERS: u64 = 14;
const BODY_KEY_NETWORK_ID: u64 = 15;
const WITNESS_KEY_VKEY: u64 = 0;
fn blake2b224(data: &[u8]) -> [u8; 28] {
use blake2::digest::consts::U28;
use blake2::digest::Digest;
use blake2::Blake2b;
Blake2b::<U28>::digest(data).into()
}
fn map_get(map: &[(PermissiveValue, PermissiveValue)], key: u64) -> Option<&PermissiveValue> {
map.iter()
.find(|(k, _)| matches!(k, PermissiveValue::Unsigned(n) if *n == key))
.map(|(_, v)| v)
}
fn as_array(v: &PermissiveValue) -> &[PermissiveValue] {
match v {
PermissiveValue::Array(items) => items,
_ => &[],
}
}
fn as_map(v: &PermissiveValue) -> Option<&[(PermissiveValue, PermissiveValue)]> {
match v {
PermissiveValue::Map(pairs) => Some(pairs),
_ => None,
}
}
#[must_use]
pub fn decode_tx_witnesses(witness_set_bytes: &[u8], tx_body_bytes: &[u8]) -> Vec<VerifyTxWitness> {
let Ok(decoded) = decode_cbor_permissive(witness_set_bytes) else {
return Vec::new();
};
let Some(witness_set) = as_map(&decoded) else {
return Vec::new();
};
let Some(vkey_value) = map_get(witness_set, WITNESS_KEY_VKEY) else {
return Vec::new();
};
let tx_hash = blake2b256(tx_body_bytes);
let mut out: Vec<VerifyTxWitness> = Vec::new();
for entry in as_array(vkey_value) {
let pair = as_array(entry);
let vkey = pair.first();
let signature = pair.get(1);
let vkey_bytes = match vkey {
Some(PermissiveValue::Bytes(b)) if b.len() == ED25519_PUBLIC_KEY_LENGTH => b.as_slice(),
_ => continue,
};
let sig_bytes = match signature {
Some(PermissiveValue::Bytes(b)) if b.len() == ED25519_SIGNATURE_LENGTH => {
Some(b.as_slice())
}
_ => None,
};
let signature_valid = match sig_bytes {
Some(sig) => ed25519_verify(vkey_bytes, &tx_hash, sig),
None => false,
};
out.push(VerifyTxWitness {
vkey: crate::hex::encode(vkey_bytes),
key_hash: crate::hex::encode(&blake2b224(vkey_bytes)),
signature_valid,
});
}
out
}
fn count_script_witnesses(witness_set_bytes: &[u8]) -> u64 {
let Ok(decoded) = decode_cbor_permissive(witness_set_bytes) else {
return 0;
};
let Some(witness_set) = as_map(&decoded) else {
return 0;
};
let mut count: u64 = 0;
for (key, value) in witness_set {
if matches!(key, PermissiveValue::Unsigned(n) if *n == WITNESS_KEY_VKEY) {
continue;
}
count += as_array(value).len() as u64;
}
count
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TxSummaryError {
#[error("MALFORMED_CBOR: {0}")]
Malformed(String),
}
pub fn decode_tx_summary(
tx_body_bytes: &[u8],
witness_set_bytes: &[u8],
network: CardanoNetwork,
) -> Result<VerifyTxSummary, TxSummaryError> {
let decoded = decode_cbor_permissive(tx_body_bytes)
.map_err(|e| TxSummaryError::Malformed(e.to_string()))?;
let body = as_map(&decoded)
.ok_or_else(|| TxSummaryError::Malformed("tx body is not a CBOR map".to_string()))?;
let inputs = map_get(body, BODY_KEY_INPUTS)
.map(as_array)
.unwrap_or_default();
let outputs_raw = map_get(body, BODY_KEY_OUTPUTS)
.map(as_array)
.unwrap_or_default();
let mut outputs: Vec<VerifyTxOutput> = Vec::with_capacity(outputs_raw.len());
let mut total_output: u128 = 0;
for o in outputs_raw {
let (address_bytes, lovelace) = read_output(o)?;
total_output = total_output.saturating_add(u128::from(lovelace));
outputs.push(VerifyTxOutput {
address: encode_cardano_address(&address_bytes, network)?,
lovelace: lovelace.to_string(),
});
}
let required_signers: Vec<String> = map_get(body, BODY_KEY_REQUIRED_SIGNERS)
.map(as_array)
.unwrap_or_default()
.iter()
.filter_map(|s| match s {
PermissiveValue::Bytes(b) => Some(crate::hex::encode(b)),
_ => None,
})
.collect();
let fee_lovelace = coin_to_string(map_get(body, BODY_KEY_FEE))?;
let invalid_before = map_get(body, BODY_KEY_INVALID_BEFORE).and_then(as_u64);
let invalid_hereafter = map_get(body, BODY_KEY_INVALID_HEREAFTER).and_then(as_u64);
let network_id = map_get(body, BODY_KEY_NETWORK_ID).and_then(as_u64);
Ok(VerifyTxSummary {
fee_lovelace,
input_count: inputs.len() as u64,
output_count: outputs.len() as u64,
total_output_lovelace: total_output.to_string(),
script_witness_count: count_script_witnesses(witness_set_bytes),
outputs,
invalid_before,
invalid_hereafter,
required_signer_key_hashes: if required_signers.is_empty() {
None
} else {
Some(required_signers)
},
network_id,
})
}
fn read_output(output: &PermissiveValue) -> Result<(Vec<u8>, u64), TxSummaryError> {
let (address, amount) = match output {
PermissiveValue::Array(items) => (items.first(), items.get(1)),
PermissiveValue::Map(pairs) => (map_get(pairs, 0), map_get(pairs, 1)),
_ => {
return Err(TxSummaryError::Malformed(
"tx output is neither a CBOR array nor a CBOR map".to_string(),
))
}
};
let address_bytes = match address {
Some(PermissiveValue::Bytes(b)) => b.clone(),
_ => {
return Err(TxSummaryError::Malformed(
"tx output address is not a byte string".to_string(),
))
}
};
let lovelace = match amount {
Some(PermissiveValue::Array(items)) => to_u64(items.first())?,
other => to_u64(other)?,
};
Ok((address_bytes, lovelace))
}
fn coin_to_string(v: Option<&PermissiveValue>) -> Result<String, TxSummaryError> {
Ok(to_u64(v)?.to_string())
}
fn to_u64(v: Option<&PermissiveValue>) -> Result<u64, TxSummaryError> {
match v {
Some(PermissiveValue::Unsigned(n)) => Ok(*n),
_ => Err(TxSummaryError::Malformed(
"expected a non-negative integer coin value".to_string(),
)),
}
}
fn as_u64(v: &PermissiveValue) -> Option<u64> {
match v {
PermissiveValue::Unsigned(n) => Some(*n),
_ => None,
}
}
fn encode_cardano_address(
address_bytes: &[u8],
network: CardanoNetwork,
) -> Result<String, TxSummaryError> {
let header = *address_bytes
.first()
.ok_or_else(|| TxSummaryError::Malformed("empty address byte string".to_string()))?;
let address_type = header >> 4;
let network_nibble = header & 0x0f;
let is_stake = address_type == 14 || address_type == 15;
let is_testnet = match network_nibble {
0 => true,
1 => false,
_ => network == CardanoNetwork::Preprod,
};
let base = if is_stake { "stake" } else { "addr" };
let hrp = if is_testnet {
format!("{base}_test")
} else {
base.to_string()
};
bech32_encode_no_limit(&hrp, address_bytes)
.map_err(|e| TxSummaryError::Malformed(format!("address bech32 encode failed: {e}")))
}