Skip to main content

urma_chain/
decode.rs

1use crate::observation::Chain;
2use crate::validation::{BlockValidationError, validate_block_integrity};
3use bitcoin::{
4    Block, BlockHash, Transaction,
5    consensus::{deserialize, deserialize_partial},
6};
7use litecoin::block::MwebBlock;
8use litecoin::consensus::{Decodable, Encodable, serialize};
9
10#[derive(Clone, Copy)]
11enum Encoding {
12    Core,
13    Esplora,
14}
15
16/// An integrity-checked transparent block projection with its original
17/// transaction encodings, in the same order as `Block::txdata`.
18///
19/// Litecoin HogEx metadata is retained in the raw transactions, even though it
20/// is stripped from the transparent projection. The block extension suffix is
21/// not part of any transaction. Extension parsing does not validate MWEB consensus.
22#[derive(Debug)]
23pub struct DecodedBlock {
24    block: Block,
25    raw_transactions: Vec<Vec<u8>>,
26}
27
28impl DecodedBlock {
29    pub fn block(&self) -> &Block {
30        &self.block
31    }
32
33    pub fn raw_transactions(&self) -> &[Vec<u8>] {
34        &self.raw_transactions
35    }
36
37    pub fn into_parts(self) -> (Block, Vec<Vec<u8>>) {
38        (self.block, self.raw_transactions)
39    }
40}
41
42#[derive(Debug)]
43pub enum DecodeError {
44    LimitExceeded,
45    Bitcoin(bitcoin::consensus::encode::Error),
46    Litecoin(litecoin::consensus::encode::Error),
47    Integrity(BlockValidationError),
48    NonCanonical,
49    UnsupportedMwebTransaction,
50    InvalidHogEx,
51}
52
53impl std::fmt::Display for DecodeError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::LimitExceeded => {
57                f.write_str("raw chain data exceeds 8,000,000 byte client limit")
58            }
59            Self::Bitcoin(cause) => write!(f, "decode Bitcoin data: {cause}"),
60            Self::Litecoin(cause) => write!(f, "decode Litecoin data: {cause}"),
61            Self::Integrity(cause) => std::fmt::Display::fmt(cause, f),
62            Self::NonCanonical => f.write_str("noncanonical or trailing chain data"),
63            Self::UnsupportedMwebTransaction => {
64                f.write_str("embedded confidential MWEB transactions are unsupported")
65            }
66            Self::InvalidHogEx => f.write_str("invalid HogEx placement"),
67        }
68    }
69}
70
71impl std::error::Error for DecodeError {
72    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
73        match self {
74            Self::Bitcoin(cause) => Some(cause),
75            Self::Litecoin(cause) => Some(cause),
76            Self::Integrity(cause) => Some(cause),
77            _ => None,
78        }
79    }
80}
81
82pub fn block(raw: &[u8], chain: Chain, expected_hash: BlockHash) -> Result<Block, DecodeError> {
83    block_with_transactions(raw, chain, expected_hash).map(|decoded| decoded.block)
84}
85
86pub fn esplora_block(
87    raw: &[u8],
88    chain: Chain,
89    expected_hash: BlockHash,
90) -> Result<Block, DecodeError> {
91    esplora_block_with_transactions(raw, chain, expected_hash).map(|decoded| decoded.block)
92}
93
94/// Decodes strict Core block encoding and retains each original transaction.
95pub fn block_with_transactions(
96    raw: &[u8],
97    chain: Chain,
98    expected_hash: BlockHash,
99) -> Result<DecodedBlock, DecodeError> {
100    decode_block(raw, chain, expected_hash, Encoding::Core)
101}
102
103/// Decodes Esplora block encoding and retains each original transaction.
104pub fn esplora_block_with_transactions(
105    raw: &[u8],
106    chain: Chain,
107    expected_hash: BlockHash,
108) -> Result<DecodedBlock, DecodeError> {
109    decode_block(raw, chain, expected_hash, Encoding::Esplora)
110}
111
112fn decode_block(
113    raw: &[u8],
114    chain: Chain,
115    expected_hash: BlockHash,
116    encoding: Encoding,
117) -> Result<DecodedBlock, DecodeError> {
118    bounded(raw)?;
119    let block = match chain {
120        Chain::BitcoinMainnet | Chain::BitcoinRegtest | Chain::BitcoinTestnet4 => {
121            bitcoin_decoded_block(raw)?
122        }
123        Chain::LitecoinMainnet | Chain::LitecoinTestnet => litecoin_block(raw, encoding)?,
124    };
125    validate_block_integrity(block.block(), expected_hash).map_err(DecodeError::Integrity)?;
126    Ok(block)
127}
128
129fn bitcoin_decoded_block(raw: &[u8]) -> Result<DecodedBlock, DecodeError> {
130    // Keep the existing whole-block decoder and its EOF/error semantics. The
131    // second pass measures consumed spans rather than serializing the projection.
132    let block: Block = deserialize(raw).map_err(DecodeError::Bitcoin)?;
133    let mut remaining = &raw[bitcoin::block::Header::SIZE..];
134    let (_, prefix_bytes) = deserialize_partial::<bitcoin::consensus::encode::VarInt>(remaining)
135        .map_err(DecodeError::Bitcoin)?;
136    remaining = &remaining[prefix_bytes..];
137    let mut raw_transactions = Vec::new();
138    for _tx in &block.txdata {
139        let (_, consumed) =
140            deserialize_partial::<Transaction>(remaining).map_err(DecodeError::Bitcoin)?;
141        raw_transactions.push(remaining[..consumed].to_vec());
142        remaining = &remaining[consumed..];
143    }
144    Ok(DecodedBlock {
145        block,
146        raw_transactions,
147    })
148}
149
150pub fn bitcoin_block(raw: &[u8], expected_hash: BlockHash) -> Result<Block, DecodeError> {
151    block(raw, Chain::BitcoinRegtest, expected_hash)
152}
153
154pub fn transaction(raw: &[u8], chain: Chain) -> Result<Transaction, DecodeError> {
155    bounded(raw)?;
156    match chain {
157        Chain::BitcoinMainnet | Chain::BitcoinRegtest | Chain::BitcoinTestnet4 => {
158            deserialize(raw).map_err(DecodeError::Bitcoin)
159        }
160        Chain::LitecoinMainnet | Chain::LitecoinTestnet => {
161            let mut remaining = raw;
162            let tx = canonical::<litecoin::Transaction>(&mut remaining)?;
163            finished(remaining)?;
164            transparent(tx)
165        }
166    }
167}
168
169fn bounded(raw: &[u8]) -> Result<(), DecodeError> {
170    if raw.len() > 8_000_000 {
171        return Err(DecodeError::LimitExceeded);
172    }
173    Ok(())
174}
175
176fn canonical<T: Decodable + Encodable>(remaining: &mut &[u8]) -> Result<T, DecodeError> {
177    let before = *remaining;
178    let value = T::consensus_decode_from_finite_reader(remaining).map_err(DecodeError::Litecoin)?;
179    if serialize(&value) != before[..before.len() - remaining.len()] {
180        return Err(DecodeError::NonCanonical);
181    }
182    Ok(value)
183}
184
185fn finished(remaining: &[u8]) -> Result<(), DecodeError> {
186    if !remaining.is_empty() {
187        return Err(DecodeError::NonCanonical);
188    }
189    Ok(())
190}
191
192fn transparent(mut tx: litecoin::Transaction) -> Result<Transaction, DecodeError> {
193    if tx.mw_tx.iter().count() != 0 || tx.input.is_empty() {
194        return Err(DecodeError::UnsupportedMwebTransaction);
195    }
196    tx.is_hog_ex = false;
197    deserialize(&serialize(&tx)).map_err(DecodeError::Bitcoin)
198}
199
200fn litecoin_block(raw: &[u8], encoding: Encoding) -> Result<DecodedBlock, DecodeError> {
201    let mut remaining = raw;
202    let header = canonical::<litecoin::block::Header>(&mut remaining)?;
203    let transactions_start = remaining;
204    let transactions = canonical::<Vec<litecoin::Transaction>>(&mut remaining)?;
205    let mut transaction_bytes = &transactions_start[..transactions_start.len() - remaining.len()];
206    canonical::<litecoin::consensus::encode::VarInt>(&mut transaction_bytes)?;
207    let count = transactions.len();
208    let mut txdata = Vec::new();
209    let mut raw_transactions = Vec::new();
210    for (index, tx) in transactions.into_iter().enumerate() {
211        // The canonical vector check already proved these original Litecoin
212        // encodings byte-identical to the consumed span. Use their lengths only
213        // to split the original bytes, before `transparent` strips HogEx metadata.
214        let length = serialize(&tx).len();
215        raw_transactions.push(transaction_bytes[..length].to_vec());
216        transaction_bytes = &transaction_bytes[length..];
217        if tx.is_hog_ex {
218            if index == 0 || index + 1 != count {
219                return Err(DecodeError::InvalidHogEx);
220            }
221            if !remaining.is_empty() || matches!(encoding, Encoding::Core) {
222                match canonical::<u8>(&mut remaining)? {
223                    0 => (),
224                    1 => {
225                        canonical::<MwebBlock>(&mut remaining)?;
226                    }
227                    _ => return Err(DecodeError::NonCanonical),
228                }
229            }
230        }
231        txdata.push(transparent(tx)?);
232    }
233    finished(remaining)?;
234    Ok(DecodedBlock {
235        block: Block {
236            header: deserialize(&serialize(&header)).map_err(DecodeError::Bitcoin)?,
237            txdata,
238        },
239        raw_transactions,
240    })
241}