use core::fmt;
use core::str::FromStr;
use super::Error;
use super::zip244::{ShieldedDigests, shielded_digests, txid_digest};
use crate::encoding::{self, Reader, write_compact_size, write_var_bytes};
pub const TX_VERSION_5: u32 = 5;
pub const V5_VERSION_GROUP_ID: u32 = 0x26a7_270a;
pub const OVERWINTERED_FLAG: u32 = 0x8000_0000;
pub const SEQUENCE_FINAL: u32 = 0xffff_ffff;
pub const SEQUENCE_ENABLE_LOCKTIME: u32 = 0xffff_fffe;
pub const LOCKTIME_THRESHOLD: u32 = 500_000_000;
const V5_HEADER: u32 = TX_VERSION_5 | OVERWINTERED_FLAG;
const MIN_INPUT_SIZE: usize = 32 + 4 + 1 + 4;
const MIN_OUTPUT_SIZE: usize = 8 + 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Txid(pub [u8; 32]);
impl Txid {
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub const fn to_bytes(&self) -> [u8; 32] {
self.0
}
pub fn from_display_hex(s: &str) -> Result<Self, Error> {
if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(Error::InvalidTxid);
}
let mut bytes: [u8; 32] = encoding::decode_hex_array(s, "txid").map_err(|_| Error::InvalidTxid)?;
bytes.reverse();
Ok(Self(bytes))
}
pub fn to_display_hex(&self) -> String {
let mut bytes = self.0;
bytes.reverse();
encoding::to_hex(&bytes)
}
pub fn is_txid(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
}
impl fmt::Display for Txid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_display_hex())
}
}
impl fmt::Debug for Txid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Txid({})", self.to_display_hex())
}
}
impl FromStr for Txid {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_display_hex(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OutPoint {
pub txid: Txid,
pub vout: u32,
}
impl OutPoint {
pub fn to_bytes(&self) -> [u8; 36] {
let mut out = [0u8; 36];
out[..32].copy_from_slice(&self.txid.0);
out[32..].copy_from_slice(&self.vout.to_le_bytes());
out
}
}
impl fmt::Display for OutPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.txid, self.vout)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxIn {
pub prevout: OutPoint,
pub script_sig: Vec<u8>,
pub sequence: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxOut {
pub value: i64,
pub script_pubkey: Vec<u8>,
}
impl TxOut {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(9 + self.script_pubkey.len());
self.write(&mut out);
out
}
pub(crate) fn write(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.value.to_le_bytes());
write_var_bytes(out, &self.script_pubkey);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransparentTx {
pub branch_id: u32,
pub lock_time: u32,
pub expiry_height: u32,
pub inputs: Vec<TxIn>,
pub outputs: Vec<TxOut>,
}
impl TransparentTx {
pub fn header_bytes(&self) -> [u8; 20] {
let mut out = [0u8; 20];
out[0..4].copy_from_slice(&V5_HEADER.to_le_bytes());
out[4..8].copy_from_slice(&V5_VERSION_GROUP_ID.to_le_bytes());
out[8..12].copy_from_slice(&self.branch_id.to_le_bytes());
out[12..16].copy_from_slice(&self.lock_time.to_le_bytes());
out[16..20].copy_from_slice(&self.expiry_height.to_le_bytes());
out
}
fn write_transparent(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.header_bytes());
write_compact_size(out, self.inputs.len() as u64);
for input in &self.inputs {
out.extend_from_slice(&input.prevout.to_bytes());
write_var_bytes(out, &input.script_sig);
out.extend_from_slice(&input.sequence.to_le_bytes());
}
write_compact_size(out, self.outputs.len() as u64);
for output in &self.outputs {
output.write(out);
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
self.write_transparent(&mut out);
out.extend_from_slice(&[0, 0, 0]);
out
}
pub fn to_hex(&self) -> String {
encoding::to_hex(&self.to_bytes())
}
pub fn txid(&self) -> Txid {
Txid(txid_digest(self, &ShieldedDigests::empty()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedTx {
pub tx: TransparentTx,
pub shielded_raw: Vec<u8>,
}
impl ParsedTx {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(256 + self.shielded_raw.len());
self.tx.write_transparent(&mut out);
out.extend_from_slice(&self.shielded_raw);
out
}
pub fn is_transparent_only(&self) -> bool {
self.shielded_raw == [0, 0, 0]
}
pub fn shielded_digests(&self) -> Result<ShieldedDigests, Error> {
shielded_digests(&self.shielded_raw)
}
pub fn txid(&self) -> Result<Txid, Error> {
Ok(Txid(txid_digest(&self.tx, &self.shielded_digests()?)))
}
}
pub fn parse_tx(data: &[u8]) -> Result<ParsedTx, Error> {
let mut r = Reader::new(data);
if r.u32()? != V5_HEADER {
return Err(Error::UnsupportedVersion);
}
if r.u32()? != V5_VERSION_GROUP_ID {
return Err(Error::BadVersionGroupId);
}
let branch_id = r.u32()?;
let lock_time = r.u32()?;
let expiry_height = r.u32()?;
let n_inputs = r.count(MIN_INPUT_SIZE)?;
let mut inputs = Vec::with_capacity(n_inputs);
for _ in 0..n_inputs {
let txid = Txid(r.array()?);
let vout = r.u32()?;
let script_sig = r.var_bytes()?.to_vec();
let sequence = r.u32()?;
inputs.push(TxIn { prevout: OutPoint { txid, vout }, script_sig, sequence });
}
let n_outputs = r.count(MIN_OUTPUT_SIZE)?;
let mut outputs = Vec::with_capacity(n_outputs);
for _ in 0..n_outputs {
let value = r.i64()?;
let script_pubkey = r.var_bytes()?.to_vec();
outputs.push(TxOut { value, script_pubkey });
}
Ok(ParsedTx {
tx: TransparentTx { branch_id, lock_time, expiry_height, inputs, outputs },
shielded_raw: r.rest().to_vec(),
})
}
pub fn transparent_txid(data: &[u8]) -> Option<Txid> {
let parsed = parse_tx(data).ok()?;
parsed.is_transparent_only().then(|| parsed.tx.txid())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawInput {
pub prevout: Option<OutPoint>,
pub script_sig: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedTx {
pub txid: Txid,
pub version: u32,
pub inputs: Vec<RawInput>,
}
fn raw_input(prevout: OutPoint, script_sig: Vec<u8>) -> RawInput {
let coinbase = prevout.txid.0 == [0u8; 32] && prevout.vout == 0xffff_ffff;
RawInput { prevout: (!coinbase).then_some(prevout), script_sig }
}
const V3_VERSION_GROUP_ID: u32 = 0x03c4_8270;
const V4_VERSION_GROUP_ID: u32 = 0x892f_2085;
const JOINSPLIT_BCTV14_SIZE: usize = 1802;
const JOINSPLIT_GROTH16_SIZE: usize = 1698;
const SAPLING_SPEND_V4_SIZE: usize = 384;
const SAPLING_OUTPUT_V4_SIZE: usize = 948;
pub fn decode_tx(data: &[u8]) -> Result<DecodedTx, Error> {
use sha2::{Digest, Sha256};
let mut r = Reader::new(data);
let header = r.u32()?;
let version = header & 0x7fff_ffff;
if version == TX_VERSION_5 {
let parsed = parse_tx(data)?;
let txid = parsed.txid()?;
let inputs = parsed.tx.inputs.into_iter().map(|i| raw_input(i.prevout, i.script_sig)).collect();
return Ok(DecodedTx { txid, version, inputs });
}
if !(1..=4).contains(&version) {
return Err(Error::UnsupportedTxVersion(version));
}
let overwintered = header & OVERWINTERED_FLAG != 0;
if overwintered != (version >= 3) {
return Err(Error::OverwinteredFlag { version, set: version >= 3 });
}
if overwintered {
let expected = if version == 3 { V3_VERSION_GROUP_ID } else { V4_VERSION_GROUP_ID };
if r.u32()? != expected {
return Err(Error::BadPreV5VersionGroupId(version));
}
}
let n_inputs = r.count(MIN_INPUT_SIZE)?;
let mut inputs = Vec::with_capacity(n_inputs);
for _ in 0..n_inputs {
let txid = Txid(r.array()?);
let vout = r.u32()?;
let script_sig = r.var_bytes()?.to_vec();
r.u32()?; inputs.push(raw_input(OutPoint { txid, vout }, script_sig));
}
let n_outputs = r.count(MIN_OUTPUT_SIZE)?;
for _ in 0..n_outputs {
r.take(8)?; r.var_bytes()?; }
r.u32()?; if version >= 3 {
r.u32()?; }
let mut sapling_descriptions = 0usize;
if version == 4 {
r.take(8)?; let spends = r.count(SAPLING_SPEND_V4_SIZE)?;
r.take(spends * SAPLING_SPEND_V4_SIZE)?;
let outputs = r.count(SAPLING_OUTPUT_V4_SIZE)?;
r.take(outputs * SAPLING_OUTPUT_V4_SIZE)?;
sapling_descriptions = spends + outputs;
}
if version >= 2 {
let size = if version == 4 { JOINSPLIT_GROTH16_SIZE } else { JOINSPLIT_BCTV14_SIZE };
let joinsplits = r.count(size)?;
r.take(joinsplits * size)?;
if joinsplits > 0 {
r.take(32 + 64)?; }
}
if sapling_descriptions > 0 {
r.take(64)?; }
if !r.is_done() {
return Err(Error::TrailingTxData(version));
}
let txid = Txid(Sha256::digest(Sha256::digest(data)).into());
Ok(DecodedTx { txid, version, inputs })
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn round_trips_a_transparent_only_transaction() {
let tx = TransparentTx {
branch_id: 0x37a5_165b,
lock_time: 123,
expiry_height: 0,
inputs: vec![TxIn {
prevout: OutPoint { txid: Txid::from_display_hex(&"ab".repeat(32)).unwrap(), vout: 1 },
script_sig: vec![1, 2, 3],
sequence: SEQUENCE_ENABLE_LOCKTIME,
}],
outputs: vec![TxOut { value: 99_990_000, script_pubkey: vec![0x51] }],
};
let parsed = parse_tx(&tx.to_bytes()).unwrap();
assert_eq!(parsed.shielded_raw, vec![0, 0, 0]);
assert_eq!(parsed.tx, tx);
assert_eq!(parsed.to_bytes(), tx.to_bytes());
assert_eq!(parsed.txid().unwrap(), tx.txid());
assert_eq!(transparent_txid(&tx.to_bytes()), Some(tx.txid()));
}
#[test]
fn txids_display_byte_reversed() {
let mut bytes = [0x11u8; 32];
bytes[31] = 0x22;
assert_eq!(Txid(bytes).to_display_hex(), format!("22{}", "11".repeat(31)));
assert_eq!(Txid::from_display_hex(&Txid(bytes).to_display_hex()).unwrap(), Txid(bytes));
assert!(Txid::from_display_hex("0x").is_err());
assert!(Txid::from_display_hex(&"g".repeat(64)).is_err());
}
#[test]
fn rejects_other_versions_and_truncation() {
assert_eq!(parse_tx(&[0x04, 0, 0, 0x80]), Err(Error::UnsupportedVersion));
let mut bad_group = V5_HEADER.to_le_bytes().to_vec();
bad_group.extend_from_slice(&[0; 4]);
assert_eq!(parse_tx(&bad_group), Err(Error::BadVersionGroupId));
let tx = TransparentTx { branch_id: 1, lock_time: 0, expiry_height: 0, inputs: vec![], outputs: vec![] };
let bytes = tx.to_bytes();
for cut in 0..bytes.len() - 3 {
assert!(parse_tx(&bytes[..cut]).is_err(), "cut {cut}");
}
}
}