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