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