Skip to main content

urma_chain/
decode.rs

1use crate::observation::Chain;
2use crate::validation::{BlockValidationError, validate_block_integrity};
3use bitcoin::{Block, BlockHash, Transaction, consensus::deserialize};
4use litecoin::block::MwebBlock;
5use litecoin::consensus::{Decodable, Encodable, serialize};
6
7#[derive(Clone, Copy)]
8enum Encoding {
9    Core,
10    Esplora,
11}
12
13#[derive(Debug)]
14pub enum DecodeError {
15    LimitExceeded,
16    Bitcoin(bitcoin::consensus::encode::Error),
17    Litecoin(litecoin::consensus::encode::Error),
18    Integrity(BlockValidationError),
19    NonCanonical,
20    UnsupportedMwebTransaction,
21    InvalidHogEx,
22}
23
24impl std::fmt::Display for DecodeError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::LimitExceeded => {
28                f.write_str("raw chain data exceeds 8,000,000 byte client limit")
29            }
30            Self::Bitcoin(cause) => write!(f, "decode Bitcoin data: {cause}"),
31            Self::Litecoin(cause) => write!(f, "decode Litecoin data: {cause}"),
32            Self::Integrity(cause) => std::fmt::Display::fmt(cause, f),
33            Self::NonCanonical => f.write_str("noncanonical or trailing chain data"),
34            Self::UnsupportedMwebTransaction => {
35                f.write_str("embedded confidential MWEB transactions are unsupported")
36            }
37            Self::InvalidHogEx => f.write_str("invalid HogEx placement"),
38        }
39    }
40}
41
42impl std::error::Error for DecodeError {
43    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44        match self {
45            Self::Bitcoin(cause) => Some(cause),
46            Self::Litecoin(cause) => Some(cause),
47            Self::Integrity(cause) => Some(cause),
48            _ => None,
49        }
50    }
51}
52
53pub fn block(raw: &[u8], chain: Chain, expected_hash: BlockHash) -> Result<Block, DecodeError> {
54    decode_block(raw, chain, expected_hash, Encoding::Core)
55}
56
57pub fn esplora_block(
58    raw: &[u8],
59    chain: Chain,
60    expected_hash: BlockHash,
61) -> Result<Block, DecodeError> {
62    decode_block(raw, chain, expected_hash, Encoding::Esplora)
63}
64
65fn decode_block(
66    raw: &[u8],
67    chain: Chain,
68    expected_hash: BlockHash,
69    encoding: Encoding,
70) -> Result<Block, DecodeError> {
71    bounded(raw)?;
72    let block = match chain {
73        Chain::BitcoinRegtest | Chain::BitcoinTestnet4 => {
74            deserialize(raw).map_err(DecodeError::Bitcoin)?
75        }
76        Chain::LitecoinMainnet | Chain::LitecoinTestnet => litecoin_block(raw, encoding)?,
77    };
78    validate_block_integrity(&block, expected_hash).map_err(DecodeError::Integrity)?;
79    Ok(block)
80}
81
82pub fn bitcoin_block(raw: &[u8], expected_hash: BlockHash) -> Result<Block, DecodeError> {
83    block(raw, Chain::BitcoinRegtest, expected_hash)
84}
85
86pub fn transaction(raw: &[u8], chain: Chain) -> Result<Transaction, DecodeError> {
87    bounded(raw)?;
88    match chain {
89        Chain::BitcoinRegtest | Chain::BitcoinTestnet4 => {
90            deserialize(raw).map_err(DecodeError::Bitcoin)
91        }
92        Chain::LitecoinMainnet | Chain::LitecoinTestnet => {
93            let mut remaining = raw;
94            let tx = canonical::<litecoin::Transaction>(&mut remaining)?;
95            finished(remaining)?;
96            transparent(tx)
97        }
98    }
99}
100
101fn bounded(raw: &[u8]) -> Result<(), DecodeError> {
102    if raw.len() > 8_000_000 {
103        return Err(DecodeError::LimitExceeded);
104    }
105    Ok(())
106}
107
108fn canonical<T: Decodable + Encodable>(remaining: &mut &[u8]) -> Result<T, DecodeError> {
109    let before = *remaining;
110    let value = T::consensus_decode_from_finite_reader(remaining).map_err(DecodeError::Litecoin)?;
111    if serialize(&value) != before[..before.len() - remaining.len()] {
112        return Err(DecodeError::NonCanonical);
113    }
114    Ok(value)
115}
116
117fn finished(remaining: &[u8]) -> Result<(), DecodeError> {
118    if !remaining.is_empty() {
119        return Err(DecodeError::NonCanonical);
120    }
121    Ok(())
122}
123
124fn transparent(mut tx: litecoin::Transaction) -> Result<Transaction, DecodeError> {
125    if tx.mw_tx.iter().count() != 0 || tx.input.is_empty() {
126        return Err(DecodeError::UnsupportedMwebTransaction);
127    }
128    tx.is_hog_ex = false;
129    deserialize(&serialize(&tx)).map_err(DecodeError::Bitcoin)
130}
131
132fn litecoin_block(raw: &[u8], encoding: Encoding) -> Result<Block, DecodeError> {
133    let mut remaining = raw;
134    let header = canonical::<litecoin::block::Header>(&mut remaining)?;
135    let transactions = canonical::<Vec<litecoin::Transaction>>(&mut remaining)?;
136    let count = transactions.len();
137    let mut txdata = Vec::new();
138    for (index, tx) in transactions.into_iter().enumerate() {
139        if tx.is_hog_ex {
140            if index == 0 || index + 1 != count {
141                return Err(DecodeError::InvalidHogEx);
142            }
143            if !remaining.is_empty() || matches!(encoding, Encoding::Core) {
144                match canonical::<u8>(&mut remaining)? {
145                    0 => (),
146                    1 => {
147                        canonical::<MwebBlock>(&mut remaining)?;
148                    }
149                    _ => return Err(DecodeError::NonCanonical),
150                }
151            }
152        }
153        txdata.push(transparent(tx)?);
154    }
155    finished(remaining)?;
156    Ok(Block {
157        header: deserialize(&serialize(&header)).map_err(DecodeError::Bitcoin)?,
158        txdata,
159    })
160}