zebra_chain/transparent.rs
1//! Transparent-related (Bitcoin-inherited) functionality.
2
3mod address;
4mod keys;
5mod opcodes;
6mod script;
7mod serialize;
8mod utxo;
9
10use std::{collections::HashMap, fmt, iter, ops::AddAssign};
11
12use zcash_transparent::{address::TransparentAddress, bundle::TxOut};
13
14use crate::{
15 amount::{Amount, NonNegative},
16 block,
17 parameters::Network,
18 transaction,
19};
20
21pub use address::Address;
22pub use script::Script;
23pub use serialize::{GENESIS_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN};
24pub use utxo::{
25 new_ordered_outputs, new_outputs, outputs_from_utxos, utxos_from_ordered_utxos,
26 CoinbaseSpendRestriction, OrderedUtxo, Utxo,
27};
28
29#[cfg(any(test, feature = "proptest-impl"))]
30pub use utxo::{
31 new_ordered_outputs_with_height, new_outputs_with_height, new_transaction_ordered_outputs,
32};
33
34#[cfg(any(test, feature = "proptest-impl"))]
35mod arbitrary;
36
37#[cfg(test)]
38mod tests;
39
40#[cfg(any(test, feature = "proptest-impl"))]
41use proptest_derive::Arbitrary;
42
43/// The maturity threshold for transparent coinbase outputs.
44///
45/// "A transaction MUST NOT spend a transparent output of a coinbase transaction
46/// from a block less than 100 blocks prior to the spend. Note that transparent
47/// outputs of coinbase transactions include Founders' Reward outputs and
48/// transparent Funding Stream outputs."
49/// [7.1](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus)
50//
51// TODO: change type to HeightDiff
52pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
53
54/// Extra coinbase data that identifies some coinbase transactions generated by Zebra.
55/// <https://emojipedia.org/zebra/>
56//
57// # Note
58//
59// rust-analyzer will crash in some editors when moving over an actual Zebra emoji,
60// so we encode it here. This is a known issue in emacs-lsp and other lsp implementations:
61// - https://github.com/rust-lang/rust-analyzer/issues/9121
62// - https://github.com/emacs-lsp/lsp-mode/issues/2080
63// - https://github.com/rust-lang/rust-analyzer/issues/13709
64pub const EXTRA_ZEBRA_COINBASE_DATA: &str = "z\u{1F993}";
65
66/// Arbitrary data inserted by miners into a coinbase transaction.
67//
68// TODO: rename to ExtraCoinbaseData, because height is also part of the coinbase data?
69#[derive(Clone, Eq, PartialEq)]
70#[cfg_attr(
71 any(test, feature = "proptest-impl", feature = "elasticsearch"),
72 derive(Serialize)
73)]
74pub struct CoinbaseData(
75 /// Invariant: this vec, together with the coinbase height, must be less than
76 /// 100 bytes. We enforce this by only constructing CoinbaseData fields by
77 /// parsing blocks with 100-byte data fields, and checking newly created
78 /// CoinbaseData lengths in the transaction builder.
79 pub(super) Vec<u8>,
80);
81
82#[cfg(any(test, feature = "proptest-impl"))]
83impl CoinbaseData {
84 /// Create a new `CoinbaseData` containing `data`.
85 ///
86 /// Only for use in tests.
87 pub fn new(data: Vec<u8>) -> CoinbaseData {
88 CoinbaseData(data)
89 }
90}
91
92impl AsRef<[u8]> for CoinbaseData {
93 fn as_ref(&self) -> &[u8] {
94 self.0.as_ref()
95 }
96}
97
98impl std::fmt::Debug for CoinbaseData {
99 #[allow(clippy::unwrap_in_result)]
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 let escaped = String::from_utf8(
102 self.0
103 .iter()
104 .cloned()
105 .flat_map(std::ascii::escape_default)
106 .collect(),
107 )
108 .expect("ascii::escape_default produces utf8");
109 f.debug_tuple("CoinbaseData").field(&escaped).finish()
110 }
111}
112
113/// OutPoint
114///
115/// A particular transaction output reference.
116#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
117#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
118#[cfg_attr(
119 any(test, feature = "proptest-impl", feature = "elasticsearch"),
120 derive(Serialize)
121)]
122pub struct OutPoint {
123 /// References the transaction that contains the UTXO being spent.
124 ///
125 /// # Correctness
126 ///
127 /// Consensus-critical serialization uses
128 /// [`ZcashSerialize`](crate::serialization::ZcashSerialize).
129 /// [`serde`]-based hex serialization must only be used for testing.
130 #[cfg_attr(any(test, feature = "proptest-impl"), serde(with = "hex"))]
131 pub hash: transaction::Hash,
132
133 /// Identifies which UTXO from that transaction is referenced; the
134 /// first output is 0, etc.
135 // TODO: Use OutputIndex here
136 pub index: u32,
137}
138
139impl OutPoint {
140 /// Returns a new [`OutPoint`] from an in-memory output `index`.
141 ///
142 /// # Panics
143 ///
144 /// If `index` doesn't fit in a [`u32`].
145 pub fn from_usize(hash: transaction::Hash, index: usize) -> OutPoint {
146 OutPoint {
147 hash,
148 index: index
149 .try_into()
150 .expect("valid in-memory output indexes fit in a u32"),
151 }
152 }
153}
154
155/// A transparent input to a transaction.
156#[derive(Clone, Debug, Eq, PartialEq)]
157#[cfg_attr(
158 any(test, feature = "proptest-impl", feature = "elasticsearch"),
159 derive(Serialize)
160)]
161pub enum Input {
162 /// A reference to an output of a previous transaction.
163 PrevOut {
164 /// The previous output transaction reference.
165 outpoint: OutPoint,
166 /// The script that authorizes spending `outpoint`.
167 unlock_script: Script,
168 /// The sequence number for the output.
169 sequence: u32,
170 },
171 /// New coins created by the block reward.
172 Coinbase {
173 /// The height of this block.
174 height: block::Height,
175 /// Free data inserted by miners after the block height.
176 data: CoinbaseData,
177 /// The sequence number for the output.
178 sequence: u32,
179 },
180}
181
182impl fmt::Display for Input {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 match self {
185 Input::PrevOut {
186 outpoint,
187 unlock_script,
188 ..
189 } => {
190 let mut fmter = f.debug_struct("transparent::Input::PrevOut");
191
192 fmter.field("unlock_script_len", &unlock_script.as_raw_bytes().len());
193 fmter.field("outpoint", outpoint);
194
195 fmter.finish()
196 }
197 Input::Coinbase { height, data, .. } => {
198 let mut fmter = f.debug_struct("transparent::Input::Coinbase");
199
200 fmter.field("height", height);
201 fmter.field("data_len", &data.0.len());
202
203 fmter.finish()
204 }
205 }
206 }
207}
208
209impl Input {
210 /// Returns a new coinbase input for `height` with optional `data` and `sequence`.
211 ///
212 /// # Consensus
213 ///
214 /// The combined serialized size of `height` and `data` can be at most 100 bytes.
215 ///
216 /// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
217 ///
218 /// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
219 ///
220 /// # Panics
221 ///
222 /// If the coinbase data is greater than [`MAX_COINBASE_DATA_LEN`].
223 pub fn new_coinbase(height: block::Height, data: Vec<u8>, sequence: Option<u32>) -> Input {
224 // `zcashd` includes an extra byte after the coinbase height in the coinbase data. We do
225 // that only if the data is empty to stay compliant with the following consensus rule:
226 //
227 // > A coinbase transaction script MUST have length in {2 .. 100} bytes.
228 //
229 // ## Rationale
230 //
231 // Coinbase heights < 17 are serialized as a single byte, and if there is no coinbase data,
232 // the script of a coinbase tx with such a height would consist only of this single byte,
233 // violating the consensus rule.
234 let data = if data.is_empty() { vec![0] } else { data };
235 let data_limit = MAX_COINBASE_DATA_LEN - height.coinbase_zcash_serialized_size();
236
237 assert!(
238 data.len() <= data_limit,
239 "miner data has {} bytes, which exceeds the limit of {data_limit} bytes",
240 data.len(),
241 );
242
243 Input::Coinbase {
244 height,
245 data: CoinbaseData(data),
246 // If the caller does not specify the sequence number, use a sequence number that
247 // activates the LockTime.
248 sequence: sequence.unwrap_or(0),
249 }
250 }
251
252 /// Returns the extra coinbase data in this input, if it is an [`Input::Coinbase`].
253 pub fn extra_coinbase_data(&self) -> Option<&CoinbaseData> {
254 match self {
255 Input::PrevOut { .. } => None,
256 Input::Coinbase { data, .. } => Some(data),
257 }
258 }
259
260 /// Returns the full coinbase script (the encoded height along with the
261 /// extra data) if this is an [`Input::Coinbase`]. Also returns `None` if
262 /// the coinbase is for the genesis block but does not match the expected
263 /// genesis coinbase data.
264 pub fn coinbase_script(&self) -> Option<Vec<u8>> {
265 match self {
266 Input::PrevOut { .. } => None,
267 Input::Coinbase { height, data, .. } => {
268 let mut height_and_data = Vec::new();
269 serialize::write_coinbase_height(*height, data, &mut height_and_data).ok()?;
270 height_and_data.extend(&data.0);
271 Some(height_and_data)
272 }
273 }
274 }
275
276 /// Returns the input's sequence number.
277 pub fn sequence(&self) -> u32 {
278 match self {
279 Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => *sequence,
280 }
281 }
282
283 /// Sets the input's sequence number.
284 ///
285 /// Only for use in tests.
286 #[cfg(any(test, feature = "proptest-impl"))]
287 pub fn set_sequence(&mut self, new_sequence: u32) {
288 match self {
289 Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => {
290 *sequence = new_sequence
291 }
292 }
293 }
294
295 /// If this is a [`Input::PrevOut`] input, returns this input's
296 /// [`OutPoint`]. Otherwise, returns `None`.
297 pub fn outpoint(&self) -> Option<OutPoint> {
298 if let Input::PrevOut { outpoint, .. } = self {
299 Some(*outpoint)
300 } else {
301 None
302 }
303 }
304
305 /// Set this input's [`OutPoint`].
306 ///
307 /// Should only be called on [`Input::PrevOut`] inputs.
308 ///
309 /// # Panics
310 ///
311 /// If `self` is a coinbase input.
312 #[cfg(any(test, feature = "proptest-impl"))]
313 pub fn set_outpoint(&mut self, new_outpoint: OutPoint) {
314 if let Input::PrevOut {
315 ref mut outpoint, ..
316 } = self
317 {
318 *outpoint = new_outpoint;
319 } else {
320 unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints");
321 }
322 }
323
324 /// Get the value spent by this input, by looking up its [`OutPoint`] in `outputs`.
325 /// See [`Self::value`] for details.
326 ///
327 /// # Panics
328 ///
329 /// If the provided [`Output`]s don't have this input's [`OutPoint`].
330 pub(crate) fn value_from_outputs(
331 &self,
332 outputs: &HashMap<OutPoint, Output>,
333 ) -> Amount<NonNegative> {
334 match self {
335 Input::PrevOut { outpoint, .. } => {
336 outputs
337 .get(outpoint)
338 .unwrap_or_else(|| {
339 panic!(
340 "provided Outputs (length {:?}) don't have spent {:?}",
341 outputs.len(),
342 outpoint
343 )
344 })
345 .value
346 }
347 Input::Coinbase { .. } => Amount::zero(),
348 }
349 }
350
351 /// Get the value spent by this input, by looking up its [`OutPoint`] in
352 /// [`Utxo`]s.
353 ///
354 /// This amount is added to the transaction value pool by this input.
355 ///
356 /// # Panics
357 ///
358 /// If the provided [`Utxo`]s don't have this input's [`OutPoint`].
359 pub fn value(&self, utxos: &HashMap<OutPoint, utxo::Utxo>) -> Amount<NonNegative> {
360 if let Some(outpoint) = self.outpoint() {
361 // look up the specific Output and convert it to the expected format
362 let output = utxos
363 .get(&outpoint)
364 .expect("provided Utxos don't have spent OutPoint")
365 .output
366 .clone();
367 self.value_from_outputs(&iter::once((outpoint, output)).collect())
368 } else {
369 // coinbase inputs don't need any UTXOs
370 self.value_from_outputs(&HashMap::new())
371 }
372 }
373
374 /// Get the value spent by this input, by looking up its [`OutPoint`] in
375 /// [`OrderedUtxo`]s.
376 ///
377 /// See [`Self::value`] for details.
378 ///
379 /// # Panics
380 ///
381 /// If the provided [`OrderedUtxo`]s don't have this input's [`OutPoint`].
382 pub fn value_from_ordered_utxos(
383 &self,
384 ordered_utxos: &HashMap<OutPoint, utxo::OrderedUtxo>,
385 ) -> Amount<NonNegative> {
386 if let Some(outpoint) = self.outpoint() {
387 // look up the specific Output and convert it to the expected format
388 let output = ordered_utxos
389 .get(&outpoint)
390 .expect("provided Utxos don't have spent OutPoint")
391 .utxo
392 .output
393 .clone();
394 self.value_from_outputs(&iter::once((outpoint, output)).collect())
395 } else {
396 // coinbase inputs don't need any UTXOs
397 self.value_from_outputs(&HashMap::new())
398 }
399 }
400}
401
402/// A transparent output from a transaction.
403///
404/// The most fundamental building block of a transaction is a
405/// transaction output -- the ZEC you own in your "wallet" is in
406/// fact a subset of unspent transaction outputs (or "UTXO"s) of the
407/// global UTXO set.
408///
409/// UTXOs are indivisible, discrete units of value which can only be
410/// consumed in their entirety. Thus, if I want to send you 1 ZEC and
411/// I only own one UTXO worth 2 ZEC, I would construct a transaction
412/// that spends my UTXO and sends 1 ZEC to you and 1 ZEC back to me
413/// (just like receiving change).
414#[derive(Clone, Debug, Eq, PartialEq, Hash)]
415#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Deserialize))]
416#[cfg_attr(
417 any(test, feature = "proptest-impl", feature = "elasticsearch"),
418 derive(Serialize)
419)]
420pub struct Output {
421 /// Transaction value.
422 // At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64.
423 pub value: Amount<NonNegative>,
424
425 /// The lock script defines the conditions under which this output can be spent.
426 pub lock_script: Script,
427}
428
429impl Output {
430 /// Returns a new coinbase output that pays `amount` using `lock_script`.
431 pub fn new_coinbase(amount: Amount<NonNegative>, lock_script: Script) -> Output {
432 Output {
433 value: amount,
434 lock_script,
435 }
436 }
437
438 /// Get the value contained in this output.
439 /// This amount is subtracted from the transaction value pool by this output.
440 pub fn value(&self) -> Amount<NonNegative> {
441 self.value
442 }
443
444 /// Return the destination address from a transparent output.
445 ///
446 /// Returns None if the address type is not valid or unrecognized.
447 pub fn address(&self, net: &Network) -> Option<Address> {
448 match TxOut::try_from(self).ok()?.recipient_address()? {
449 TransparentAddress::PublicKeyHash(pkh) => {
450 Some(Address::from_pub_key_hash(net.t_addr_kind(), pkh))
451 }
452 TransparentAddress::ScriptHash(sh) => {
453 Some(Address::from_script_hash(net.t_addr_kind(), sh))
454 }
455 }
456 }
457}
458
459/// A transparent output's index in its transaction.
460#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
461pub struct OutputIndex(u32);
462
463impl OutputIndex {
464 /// Create a transparent output index from the Zcash consensus integer type.
465 ///
466 /// `u32` is also the inner type.
467 pub const fn from_index(output_index: u32) -> OutputIndex {
468 OutputIndex(output_index)
469 }
470
471 /// Returns this index as the inner type.
472 pub const fn index(&self) -> u32 {
473 self.0
474 }
475
476 /// Create a transparent output index from `usize`.
477 #[allow(dead_code)]
478 pub fn from_usize(output_index: usize) -> OutputIndex {
479 OutputIndex(
480 output_index
481 .try_into()
482 .expect("the maximum valid index fits in the inner type"),
483 )
484 }
485
486 /// Return this index as `usize`.
487 #[allow(dead_code)]
488 pub fn as_usize(&self) -> usize {
489 self.0
490 .try_into()
491 .expect("the maximum valid index fits in usize")
492 }
493
494 /// Create a transparent output index from `u64`.
495 #[allow(dead_code)]
496 pub fn from_u64(output_index: u64) -> OutputIndex {
497 OutputIndex(
498 output_index
499 .try_into()
500 .expect("the maximum u64 index fits in the inner type"),
501 )
502 }
503
504 /// Return this index as `u64`.
505 #[allow(dead_code)]
506 pub fn as_u64(&self) -> u64 {
507 self.0.into()
508 }
509}
510
511impl AddAssign<u32> for OutputIndex {
512 fn add_assign(&mut self, rhs: u32) {
513 self.0 += rhs
514 }
515}