zebra_chain/transparent/serialize.rs
1//! Serializes and deserializes transparent data.
2
3use std::io;
4
5use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
6
7use crate::{
8 block::{self, Height},
9 serialization::{
10 zcash_deserialize_bytes_external_count, zcash_serialize_bytes, CompactSizeMessage,
11 FakeWriter, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashDeserializeInto,
12 ZcashSerialize,
13 },
14 transaction,
15};
16
17use super::{CoinbaseData, Input, OutPoint, Output, Script};
18
19/// The maximum length of the coinbase data.
20///
21/// Includes the encoded coinbase height, if any.
22///
23/// # Consensus
24///
25/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
26///
27/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
28pub const MAX_COINBASE_DATA_LEN: usize = 100;
29
30/// The maximum length of the encoded coinbase height.
31///
32/// # Consensus
33///
34/// > The length of heightBytes MUST be in the range {1 .. 5}. Then the encoding is the length
35/// > of heightBytes encoded as one byte, followed by heightBytes itself.
36///
37/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
38pub const MAX_COINBASE_HEIGHT_DATA_LEN: usize = 6;
39
40/// The minimum length of the coinbase data.
41///
42/// Includes the encoded coinbase height, if any.
43///
44/// # Consensus
45///
46/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
47///
48/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
49pub const MIN_COINBASE_DATA_LEN: usize = 2;
50
51/// The coinbase data for a genesis block.
52///
53/// Zcash uses the same coinbase data for the Mainnet, Testnet, and Regtest
54/// genesis blocks.
55pub const GENESIS_COINBASE_DATA: [u8; 77] = [
56 4, 255, 255, 7, 31, 1, 4, 69, 90, 99, 97, 115, 104, 48, 98, 57, 99, 52, 101, 101, 102, 56, 98,
57 55, 99, 99, 52, 49, 55, 101, 101, 53, 48, 48, 49, 101, 51, 53, 48, 48, 57, 56, 52, 98, 54, 102,
58 101, 97, 51, 53, 54, 56, 51, 97, 55, 99, 97, 99, 49, 52, 49, 97, 48, 52, 51, 99, 52, 50, 48,
59 54, 52, 56, 51, 53, 100, 51, 52,
60];
61
62impl ZcashSerialize for OutPoint {
63 fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
64 writer.write_all(&self.hash.0[..])?;
65 writer.write_u32::<LittleEndian>(self.index)?;
66 Ok(())
67 }
68}
69
70impl ZcashDeserialize for OutPoint {
71 fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
72 Ok(OutPoint {
73 hash: transaction::Hash(reader.read_32_bytes()?),
74 index: reader.read_u32::<LittleEndian>()?,
75 })
76 }
77}
78
79// Coinbase inputs include block heights (BIP34). These are not encoded
80// directly, but as a Bitcoin script that pushes the block height to the stack
81// when executed. The script data is otherwise unused. Because we want to
82// *parse* transactions into an internal representation where illegal states are
83// unrepresentable, we need just enough parsing of Bitcoin scripts to parse the
84// coinbase height and split off the rest of the (inert) coinbase data.
85
86// Starting at Network Upgrade 5, coinbase transactions also encode the block
87// height in the expiry height field. But Zebra does not use this field to
88// determine the coinbase height, because it is not present in older network
89// upgrades.
90
91/// Split `data` into a block height and remaining miner-controlled coinbase data.
92///
93/// The height may consume `0..=5` bytes at the stat of the coinbase data.
94/// The genesis block does not include an encoded coinbase height.
95///
96/// # Consensus
97///
98/// > A coinbase transaction for a *block* at *block height* greater than 0 MUST have
99/// > a script that, as its first item, encodes the *block height* `height` as follows.
100/// > For `height` in the range {1..16}, the encoding is a single byte of value
101/// > `0x50` + `height`. Otherwise, let `heightBytes` be the signed little-endian
102/// > representation of `height`, using the minimum nonzero number of bytes such that
103/// > the most significant byte is < `0x80`.
104/// > The length of `heightBytes` MUST be in the range {1..5}.
105/// > Then the encoding is the length of `heightBytes` encoded as one byte,
106/// > followed by `heightBytes` itself. This matches the encoding used by Bitcoin in the
107/// > implementation of [BIP-34] (but the description here is to be considered normative).
108///
109/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
110/// <https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki>
111pub(crate) fn parse_coinbase_height(
112 mut data: Vec<u8>,
113) -> Result<(block::Height, CoinbaseData), SerializationError> {
114 match (data.first(), data.len()) {
115 // Blocks 1 through 16 inclusive encode block height with OP_N opcodes.
116 (Some(op_n @ 0x51..=0x60), len) if len >= 1 => Ok((
117 Height((op_n - 0x50) as u32),
118 CoinbaseData(data.split_off(1)),
119 )),
120 // Blocks 17 through 128 exclusive encode block height with the `0x01` opcode.
121 // The Bitcoin encoding requires that the most significant byte is below 0x80.
122 (Some(0x01), len) if len >= 2 && data[1] < 0x80 => {
123 let h = data[1] as u32;
124 if (17..128).contains(&h) {
125 Ok((Height(h), CoinbaseData(data.split_off(2))))
126 } else {
127 Err(SerializationError::Parse("Invalid block height"))
128 }
129 }
130 // Blocks 128 through 32768 exclusive encode block height with the `0x02` opcode.
131 // The Bitcoin encoding requires that the most significant byte is below 0x80.
132 (Some(0x02), len) if len >= 3 && data[2] < 0x80 => {
133 let h = data[1] as u32 + ((data[2] as u32) << 8);
134 if (128..32_768).contains(&h) {
135 Ok((Height(h), CoinbaseData(data.split_off(3))))
136 } else {
137 Err(SerializationError::Parse("Invalid block height"))
138 }
139 }
140 // Blocks 32768 through 2**23 exclusive encode block height with the `0x03` opcode.
141 // The Bitcoin encoding requires that the most significant byte is below 0x80.
142 (Some(0x03), len) if len >= 4 && data[3] < 0x80 => {
143 let h = data[1] as u32 + ((data[2] as u32) << 8) + ((data[3] as u32) << 16);
144 if (32_768..8_388_608).contains(&h) {
145 Ok((Height(h), CoinbaseData(data.split_off(4))))
146 } else {
147 Err(SerializationError::Parse("Invalid block height"))
148 }
149 }
150 // The genesis block does not encode the block height by mistake; special case it.
151 // The first five bytes are [4, 255, 255, 7, 31], the little-endian encoding of
152 // 520_617_983.
153 //
154 // In the far future, Zcash might reach this height, and the miner might use the
155 // same coinbase data as the genesis block. So we need an updated consensus rule
156 // to handle this edge case.
157 //
158 // TODO: update this check based on the consensus rule changes in
159 // https://github.com/zcash/zips/issues/540
160 (Some(0x04), _) if data[..] == GENESIS_COINBASE_DATA[..] => {
161 Ok((Height(0), CoinbaseData(data)))
162 }
163 // As noted above, this is included for completeness.
164 // The Bitcoin encoding requires that the most significant byte is below 0x80.
165 (Some(0x04), len) if len >= 5 && data[4] < 0x80 => {
166 let h = data[1] as u32
167 + ((data[2] as u32) << 8)
168 + ((data[3] as u32) << 16)
169 + ((data[4] as u32) << 24);
170 if (8_388_608..=Height::MAX.0).contains(&h) {
171 Ok((Height(h), CoinbaseData(data.split_off(5))))
172 } else {
173 Err(SerializationError::Parse("Invalid block height"))
174 }
175 }
176 _ => Err(SerializationError::Parse(
177 "Could not parse BIP34 height in coinbase data",
178 )),
179 }
180}
181
182/// Encode `height` into a block height, as a prefix of the coinbase data.
183/// Does not write `coinbase_data`.
184///
185/// The height may produce `0..=5` initial bytes of coinbase data.
186///
187/// # Errors
188///
189/// Returns an error if the coinbase height is zero,
190/// and the `coinbase_data` does not match the Zcash mainnet and testnet genesis coinbase data.
191/// (They are identical.)
192///
193/// This check is required, because the genesis block does not include an encoded
194/// coinbase height,
195pub(crate) fn write_coinbase_height<W: io::Write>(
196 height: block::Height,
197 coinbase_data: &CoinbaseData,
198 mut w: W,
199) -> Result<(), io::Error> {
200 // We can't write this as a match statement on stable until exclusive range
201 // guards are stabilized.
202 // The Bitcoin encoding requires that the most significant byte is below 0x80,
203 // so the ranges run up to 2^{n-1} rather than 2^n.
204 if let 0 = height.0 {
205 // The genesis block's coinbase data does not have a height prefix.
206 // So we return an error if the entire coinbase data doesn't match genesis.
207 // (If we don't do this check, then deserialization will fail.)
208 //
209 // TODO: update this check based on the consensus rule changes in
210 // https://github.com/zcash/zips/issues/540
211 if coinbase_data.0 != GENESIS_COINBASE_DATA {
212 return Err(io::Error::other("invalid genesis coinbase data"));
213 }
214 } else if let h @ 1..=16 = height.0 {
215 w.write_u8(0x50 + (h as u8))?;
216 } else if let h @ 17..=127 = height.0 {
217 w.write_u8(0x01)?;
218 w.write_u8(h as u8)?;
219 } else if let h @ 128..=32_767 = height.0 {
220 w.write_u8(0x02)?;
221 w.write_u16::<LittleEndian>(h as u16)?;
222 } else if let h @ 32_768..=8_388_607 = height.0 {
223 w.write_u8(0x03)?;
224 w.write_u8(h as u8)?;
225 w.write_u8((h >> 8) as u8)?;
226 w.write_u8((h >> 16) as u8)?;
227 } else if let h @ 8_388_608..=block::Height::MAX_AS_U32 = height.0 {
228 w.write_u8(0x04)?;
229 w.write_u32::<LittleEndian>(h)?;
230 } else {
231 panic!("Invalid coinbase height");
232 }
233 Ok(())
234}
235
236impl Height {
237 /// Get the size of `Height` when serialized into a coinbase input script.
238 pub fn coinbase_zcash_serialized_size(&self) -> usize {
239 let mut writer = FakeWriter(0);
240 let empty_data = CoinbaseData(Vec::new());
241
242 write_coinbase_height(*self, &empty_data, &mut writer).expect("writer should never fail");
243 writer.0
244 }
245}
246
247impl ZcashSerialize for Input {
248 /// Serialize this transparent input.
249 ///
250 /// # Errors
251 ///
252 /// Returns an error if the coinbase height is zero,
253 /// and the coinbase data does not match the Zcash mainnet and testnet genesis coinbase data.
254 /// (They are identical.)
255 ///
256 /// This check is required, because the genesis block does not include an encoded
257 /// coinbase height,
258 fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
259 match self {
260 Input::PrevOut {
261 outpoint,
262 unlock_script,
263 sequence,
264 } => {
265 outpoint.zcash_serialize(&mut writer)?;
266 unlock_script.zcash_serialize(&mut writer)?;
267 writer.write_u32::<LittleEndian>(*sequence)?;
268 }
269 Input::Coinbase {
270 height,
271 data,
272 sequence,
273 } => {
274 writer.write_all(&[0; 32][..])?;
275 writer.write_u32::<LittleEndian>(0xffff_ffff)?;
276
277 let mut height_and_data = Vec::new();
278 write_coinbase_height(*height, data, &mut height_and_data)?;
279 height_and_data.extend(&data.0);
280 zcash_serialize_bytes(&height_and_data, &mut writer)?;
281
282 writer.write_u32::<LittleEndian>(*sequence)?;
283 }
284 }
285 Ok(())
286 }
287}
288
289impl ZcashDeserialize for Input {
290 fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
291 // This inlines the OutPoint deserialization to peek at the hash value
292 // and detect whether we have a coinbase input.
293 let bytes = reader.read_32_bytes()?;
294 if bytes == [0; 32] {
295 if reader.read_u32::<LittleEndian>()? != 0xffff_ffff {
296 return Err(SerializationError::Parse("wrong index in coinbase"));
297 }
298
299 // Read the coinbase script length and validate it against the consensus
300 // bound *before* allocating any script bytes. The generic `Vec<u8>`
301 // deserializer would otherwise allocate up to MAX_PROTOCOL_MESSAGE_LEN
302 // bytes for an attacker-controlled CompactSize length and only reject
303 // afterwards, letting a peer force multi-MiB transient allocations per
304 // bogus block.
305 //
306 // Consensus: A coinbase transaction script MUST have length in {2 .. 100} bytes.
307 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
308 let len: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?;
309 let len: usize = len.into();
310 if len > MAX_COINBASE_DATA_LEN {
311 return Err(SerializationError::Parse("coinbase data is too long"));
312 } else if len < MIN_COINBASE_DATA_LEN {
313 return Err(SerializationError::Parse("coinbase data is too short"));
314 }
315 let data = zcash_deserialize_bytes_external_count(len, &mut reader)?;
316
317 let (height, data) = parse_coinbase_height(data)?;
318
319 let sequence = reader.read_u32::<LittleEndian>()?;
320
321 Ok(Input::Coinbase {
322 height,
323 data,
324 sequence,
325 })
326 } else {
327 Ok(Input::PrevOut {
328 outpoint: OutPoint {
329 hash: transaction::Hash(bytes),
330 index: reader.read_u32::<LittleEndian>()?,
331 },
332 unlock_script: Script::zcash_deserialize(&mut reader)?,
333 sequence: reader.read_u32::<LittleEndian>()?,
334 })
335 }
336 }
337}
338
339impl ZcashSerialize for Output {
340 fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
341 self.value.zcash_serialize(&mut writer)?;
342 self.lock_script.zcash_serialize(&mut writer)?;
343 Ok(())
344 }
345}
346
347impl ZcashDeserialize for Output {
348 fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
349 let reader = &mut reader;
350
351 Ok(Output {
352 value: reader.zcash_deserialize_into()?,
353 lock_script: Script::zcash_deserialize(reader)?,
354 })
355 }
356}