Skip to main content

zebra_chain/transparent/
serialize.rs

1//! Serializes and deserializes transparent data.
2
3use std::io;
4
5use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
6use zcash_script::{opcode::Evaluable, pattern};
7use zcash_transparent::coinbase::{MAX_COINBASE_SCRIPT_LEN, MIN_COINBASE_SCRIPT_LEN};
8
9use crate::{
10    block::Height,
11    serialization::{
12        zcash_deserialize_bytes_external_count, CompactSizeMessage, ReadZcashExt,
13        SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
14    },
15    transaction,
16};
17
18use super::{Input, OutPoint, Output, Script};
19
20/// The coinbase data for a genesis block.
21///
22/// Zcash uses the same coinbase data for the Mainnet, Testnet, and Regtest
23/// genesis blocks.
24pub const GENESIS_COINBASE_SCRIPT_SIG: [u8; 77] = [
25    4, 255, 255, 7, 31, 1, 4, 69, 90, 99, 97, 115, 104, 48, 98, 57, 99, 52, 101, 101, 102, 56, 98,
26    55, 99, 99, 52, 49, 55, 101, 101, 53, 48, 48, 49, 101, 51, 53, 48, 48, 57, 56, 52, 98, 54, 102,
27    101, 97, 51, 53, 54, 56, 51, 97, 55, 99, 97, 99, 49, 52, 49, 97, 48, 52, 51, 99, 52, 50, 48,
28    54, 52, 56, 51, 53, 100, 51, 52,
29];
30
31/// Parses the BIP-34 block-height prefix of a non-genesis coinbase script and returns the height
32/// along with the trailing miner data. Also enforces the coinbase script length bound, since
33/// every production parse path for coinbase inputs goes through this function.
34///
35/// # Consensus
36///
37/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
38///
39/// > A coinbase transaction for a block at block height greater than 0 MUST have a script that, as
40/// > its first item, encodes the block height `height` as follows. For `height` in the range
41/// > {1 .. 16}, the encoding is a single byte of value `0x50` + `height`. Otherwise, let
42/// > `heightBytes` be the signed little-endian representation of `height`, using the minimum
43/// > nonzero number of bytes such that the most significant byte is < `0x80`. The length of
44/// > `heightBytes` MUST be in the range {1 .. 5}. Then the encoding is the length of `heightBytes`
45/// > encoded as one byte, followed by `heightBytes` itself. This matches the encoding used by
46/// > Bitcoin in the implementation of [BIP-34] (but the description here is to be considered
47/// > normative).
48///
49/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
50///
51/// [BIP-34]: <https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki>
52///
53/// # Strategy
54///
55/// Rather than parsing the height bytes ourselves, we read a candidate height directly off the
56/// wire (using the prefix shape to locate the bytes), then re-encode it via
57/// [`zcash_script::pattern::push_num`] — the same primitive used by
58/// [`zcash_transparent::bundle::TxIn::coinbase`] to build coinbase inputs — and require byte-exact
59/// equality. Any non-canonical input (wrong shape, non-minimal length, oversize, negative,
60/// signed-bit games) fails this check.
61pub(crate) fn parse_coinbase_height(
62    script_sig: &[u8],
63) -> Result<(Height, Vec<u8>), SerializationError> {
64    let parse_err = SerializationError::Parse;
65
66    // Length bound from the doc comment above; `zcash_transparent::TxIn::read` doesn't enforce it.
67    if script_sig.len() < MIN_COINBASE_SCRIPT_LEN {
68        return Err(parse_err("Coinbase script is too short"));
69    } else if script_sig.len() > MAX_COINBASE_SCRIPT_LEN {
70        return Err(parse_err("Coinbase script is too long"));
71    }
72
73    // Read a candidate height directly off the wire. The first byte tells us where the height
74    // bytes are; we don't validate them yet — the oracle below catches any non-canonical input.
75    let (h, len): (i64, usize) = match *script_sig
76        .first()
77        .ok_or(parse_err("Empty coinbase script"))?
78    {
79        op_n @ 0x51..=0x60 => (i64::from(op_n - 0x50), 1),
80        n @ 1..=5 => {
81            let bytes = script_sig
82                .get(1..=usize::from(n))
83                .ok_or(parse_err("Coinbase height push truncated"))?;
84            // Permissive read: zero-extend the wire bytes into an i64. The candidate is only
85            // trusted after the canonical-encode-and-compare check below.
86            let mut buf = [0u8; 8];
87            buf[..bytes.len()].copy_from_slice(bytes);
88            (i64::from_le_bytes(buf), 1 + bytes.len())
89        }
90        _ => return Err(parse_err("Invalid coinbase script prefix")),
91    };
92
93    // Oracle: re-encode the candidate the way zcash_transparent's coinbase builder does, and
94    // require byte-exact equality.
95    if script_sig
96        .get(..len)
97        .ok_or(parse_err("Coinbase script too short"))?
98        != pattern::push_num(h).to_bytes().as_slice()
99    {
100        return Err(parse_err("Non-canonical coinbase height encoding"));
101    }
102
103    let h = u32::try_from(h).map_err(|_| parse_err("Negative coinbase height"))?;
104    let height =
105        Height::try_from(h).map_err(|_| parse_err("Coinbase height exceeds Height::MAX"))?;
106
107    Ok((height, script_sig[len..].to_vec()))
108}
109
110impl ZcashSerialize for OutPoint {
111    fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
112        writer.write_all(&self.hash.0[..])?;
113        writer.write_u32::<LittleEndian>(self.index)?;
114        Ok(())
115    }
116}
117
118impl ZcashDeserialize for OutPoint {
119    fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
120        Ok(OutPoint {
121            hash: transaction::Hash(reader.read_32_bytes()?),
122            index: reader.read_u32::<LittleEndian>()?,
123        })
124    }
125}
126
127// Coinbase inputs include block heights (BIP34). These are not encoded
128// directly, but as a Bitcoin script that pushes the block height to the stack
129// when executed. The script data is otherwise unused. Because we want to
130// *parse* transactions into an internal representation where illegal states are
131// unrepresentable, we need just enough parsing of Bitcoin scripts to parse the
132// coinbase height and split off the rest of the (inert) coinbase data.
133
134// Starting at Network Upgrade 5, coinbase transactions also encode the block
135// height in the expiry height field. But Zebra does not use this field to
136// determine the coinbase height, because it is not present in older network
137// upgrades.
138
139impl ZcashSerialize for Input {
140    /// Serialize this transparent input.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the coinbase height is zero,
145    /// and the coinbase data does not match the Zcash mainnet and testnet genesis coinbase data.
146    /// (They are identical.)
147    ///
148    /// This check is required, because the genesis block does not include an encoded
149    /// coinbase height,
150    fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
151        match self {
152            Input::PrevOut {
153                outpoint,
154                unlock_script,
155                sequence,
156            } => {
157                outpoint.zcash_serialize(&mut writer)?;
158                unlock_script.zcash_serialize(&mut writer)?;
159                writer.write_u32::<LittleEndian>(*sequence)?;
160            }
161            Input::Coinbase { sequence, .. } => {
162                // Write the null prevout.
163                writer.write_all(&[0; 32][..])?;
164                writer.write_u32::<LittleEndian>(0xffff_ffff)?;
165
166                // Write the script sig containing the height and data.
167                self.coinbase_script()
168                    .ok_or_else(|| io::Error::other("invalid coinbase script sig"))?
169                    .zcash_serialize(&mut writer)?;
170
171                // Write the sequence.
172                writer.write_u32::<LittleEndian>(*sequence)?;
173            }
174        }
175        Ok(())
176    }
177}
178
179impl ZcashDeserialize for Input {
180    /// This impl is retained for tests and fixtures only: production transaction parsing
181    /// goes through `zcash_primitives`, which does not use it.
182    fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
183        // This inlines the OutPoint deserialization to peek at the hash value and detect whether we
184        // have a coinbase input.
185        let hash = reader.read_32_bytes()?;
186
187        // Coinbase inputs have a null prevout hash.
188        if hash == [0; 32] {
189            // Coinbase txs have the prevout index set to `u32::MAX`.
190            if reader.read_u32::<LittleEndian>()? != 0xffff_ffff {
191                return Err(SerializationError::Parse("Wrong index in coinbase"));
192            }
193
194            // Read the coinbase script length and validate it against the consensus
195            // bound *before* allocating any script bytes. The generic `Vec<u8>`
196            // deserializer would otherwise allocate up to MAX_PROTOCOL_MESSAGE_LEN
197            // bytes for an attacker-controlled CompactSize length and only reject
198            // afterwards, letting a peer force multi-MiB transient allocations per
199            // bogus block.
200            //
201            // The production transaction parse path enforces the same bound in
202            // `parse_coinbase_height`, which is the authoritative site; this early
203            // copy is pre-allocation hardening for this deserializer.
204            //
205            // # Consensus
206            //
207            // > A coinbase transaction script MUST have length in {2 .. 100} bytes.
208            //
209            // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
210            let len: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?;
211            let len: usize = len.into();
212            if len < MIN_COINBASE_SCRIPT_LEN {
213                return Err(SerializationError::Parse("Coinbase script is too short"));
214            } else if len > MAX_COINBASE_SCRIPT_LEN {
215                return Err(SerializationError::Parse("Coinbase script is too long"));
216            }
217            let script_sig = zcash_deserialize_bytes_external_count(len, &mut reader)?;
218
219            let (height, data) = if script_sig.as_slice() == GENESIS_COINBASE_SCRIPT_SIG {
220                (Height::MIN, GENESIS_COINBASE_SCRIPT_SIG.to_vec())
221            } else {
222                parse_coinbase_height(&script_sig)?
223            };
224
225            Ok(Input::Coinbase {
226                height,
227                data,
228                sequence: reader.read_u32::<LittleEndian>()?,
229            })
230        } else {
231            Ok(Input::PrevOut {
232                outpoint: OutPoint {
233                    hash: transaction::Hash(hash),
234                    index: reader.read_u32::<LittleEndian>()?,
235                },
236                unlock_script: Script::zcash_deserialize(&mut reader)?,
237                sequence: reader.read_u32::<LittleEndian>()?,
238            })
239        }
240    }
241}
242
243impl ZcashSerialize for Output {
244    fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
245        self.value.zcash_serialize(&mut writer)?;
246        self.lock_script.zcash_serialize(&mut writer)?;
247        Ok(())
248    }
249}
250
251impl ZcashDeserialize for Output {
252    fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
253        let reader = &mut reader;
254
255        Ok(Output {
256            value: reader.zcash_deserialize_into()?,
257            lock_script: Script::zcash_deserialize(reader)?,
258        })
259    }
260}