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}