Compare commits

...

2 Commits

Author SHA1 Message Date
likho 9024519f28 WIP: Remove transparent transactions 2024-04-22 17:52:12 +02:00
likho f748b29eca WIP: Removing transparent transactions 2024-04-22 17:18:10 +02:00
16 changed files with 4 additions and 2068 deletions

View File

@ -14,7 +14,6 @@ use crate::{
serialization::{TrustedPreallocate, MAX_PROTOCOL_MESSAGE_LEN},
sprout,
transaction::Transaction,
transparent,
value_balance::{ValueBalance, ValueBalanceError},
};
@ -81,7 +80,6 @@ impl Block {
.first()
.and_then(|tx| tx.inputs().first())
.and_then(|input| match input {
transparent::Input::Coinbase { ref height, .. } => Some(*height),
_ => None,
})
}

View File

@ -36,7 +36,6 @@ pub mod shutdown;
pub mod sprout;
pub mod subtree;
pub mod transaction;
pub mod transparent;
pub mod value_balance;
pub mod work;

View File

@ -10,7 +10,6 @@ use crate::{
parameters::{Network, NetworkUpgrade},
serialization::ZcashSerialize,
transaction::{AuthDigest, HashType, SigHash, Transaction},
transparent::{self, Script},
};
// TODO: move copied and modified code to a separate module.
@ -22,64 +21,18 @@ struct TransparentAuth<'a> {
all_prev_outputs: &'a [transparent::Output],
}
impl zp_tx::components::transparent::Authorization for TransparentAuth<'_> {
type ScriptSig = zcash_primitives::legacy::Script;
}
// In this block we convert our Output to a librustzcash to TxOut.
// (We could do the serialize/deserialize route but it's simple enough to convert manually)
impl zp_tx::sighash::TransparentAuthorizingContext for TransparentAuth<'_> {
fn input_amounts(&self) -> Vec<zp_tx::components::amount::Amount> {
self.all_prev_outputs
.iter()
.map(|prevout| {
zp_tx::components::amount::Amount::from_nonnegative_i64_le_bytes(
prevout.value.to_bytes(),
)
.expect("will not fail since it was previously validated")
})
.collect()
}
fn input_scriptpubkeys(&self) -> Vec<zcash_primitives::legacy::Script> {
self.all_prev_outputs
.iter()
.map(|prevout| {
zcash_primitives::legacy::Script(prevout.lock_script.as_raw_bytes().into())
})
.collect()
}
}
// Boilerplate mostly copied from `zcash/src/rust/src/transaction_ffi.rs` which is required
// to compute sighash.
// TODO: remove/change if they improve the API to not require this.
struct MapTransparent<'a> {
auth: TransparentAuth<'a>,
}
// struct MapTransparent<'a> {
// auth: TransparentAuth<'a>,
// }
impl<'a>
zp_tx::components::transparent::MapAuth<
zp_tx::components::transparent::Authorized,
TransparentAuth<'a>,
> for MapTransparent<'a>
{
fn map_script_sig(
&self,
s: <zp_tx::components::transparent::Authorized as zp_tx::components::transparent::Authorization>::ScriptSig,
) -> <TransparentAuth as zp_tx::components::transparent::Authorization>::ScriptSig {
s
}
fn map_authorization(
&self,
_: zp_tx::components::transparent::Authorized,
) -> TransparentAuth<'a> {
// TODO: This map should consume self, so we can move self.auth
self.auth.clone()
}
}
struct IdentityMap;
@ -189,29 +142,8 @@ pub(crate) fn convert_tx_to_librustzcash(
}
/// Convert a Zebra transparent::Output into a librustzcash one.
impl TryFrom<&transparent::Output> for zp_tx::components::TxOut {
type Error = io::Error;
#[allow(clippy::unwrap_in_result)]
fn try_from(output: &transparent::Output) -> Result<Self, Self::Error> {
let serialized_output_bytes = output
.zcash_serialize_to_vec()
.expect("zcash_primitives and Zebra transparent output formats must be compatible");
zp_tx::components::TxOut::read(&mut serialized_output_bytes.as_slice())
}
}
/// Convert a Zebra transparent::Output into a librustzcash one.
impl TryFrom<transparent::Output> for zp_tx::components::TxOut {
type Error = io::Error;
// The borrow is actually needed to use TryFrom<&transparent::Output>
#[allow(clippy::needless_borrow)]
fn try_from(output: transparent::Output) -> Result<Self, Self::Error> {
(&output).try_into()
}
}
/// Convert a Zebra Amount into a librustzcash one.
impl TryFrom<Amount<NonNegative>> for zp_tx::components::Amount {
@ -223,20 +155,8 @@ impl TryFrom<Amount<NonNegative>> for zp_tx::components::Amount {
}
/// Convert a Zebra Script into a librustzcash one.
impl From<&Script> for zcash_primitives::legacy::Script {
fn from(script: &Script) -> Self {
zcash_primitives::legacy::Script(script.as_raw_bytes().to_vec())
}
}
/// Convert a Zebra Script into a librustzcash one.
impl From<Script> for zcash_primitives::legacy::Script {
// The borrow is actually needed to use From<&Script>
#[allow(clippy::needless_borrow)]
fn from(script: Script) -> Self {
(&script).into()
}
}
/// Compute a signature hash using librustzcash.
///

View File

@ -2,9 +2,8 @@
use super::Transaction;
use crate::{parameters::NetworkUpgrade, transparent};
use crate::{parameters::NetworkUpgrade};
use crate::primitives::zcash_primitives::sighash;
static ZIP143_EXPLANATION: &str = "Invalid transaction version: after Overwinter activation transaction versions 1 and 2 are rejected";
@ -40,50 +39,3 @@ impl AsRef<[u8]> for SigHash {
}
}
pub(super) struct SigHasher<'a> {
trans: &'a Transaction,
hash_type: HashType,
network_upgrade: NetworkUpgrade,
all_previous_outputs: &'a [transparent::Output],
input_index: Option<usize>,
}
impl<'a> SigHasher<'a> {
pub fn new(
trans: &'a Transaction,
hash_type: HashType,
network_upgrade: NetworkUpgrade,
all_previous_outputs: &'a [transparent::Output],
input_index: Option<usize>,
) -> Self {
SigHasher {
trans,
hash_type,
network_upgrade,
all_previous_outputs,
input_index,
}
}
pub(super) fn sighash(self) -> SigHash {
use NetworkUpgrade::*;
match self.network_upgrade {
Genesis | BeforeOverwinter => unreachable!("{}", ZIP143_EXPLANATION),
Overwinter | Sapling | Blossom | Heartwood | Canopy | Nu5 => {
self.hash_sighash_librustzcash()
}
}
}
/// Compute a signature hash using librustzcash.
fn hash_sighash_librustzcash(&self) -> SigHash {
sighash(
self.trans,
self.hash_type,
self.network_upgrade,
self.all_previous_outputs,
self.input_index,
)
}
}

View File

@ -664,94 +664,7 @@ fn test_vec143_2() -> Result<()> {
Ok(())
}
#[test]
fn test_vec243_1() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP243_1.zcash_deserialize_into::<Transaction>()?;
let hasher = SigHasher::new(
&transaction,
HashType::ALL,
NetworkUpgrade::Sapling,
&[],
None,
);
let hash = hasher.sighash();
let expected = "63d18534de5f2d1c9e169b73f9c783718adbef5c8a7d55b5e7a37affa1dd3ff3";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&transaction,
HashType::ALL,
NetworkUpgrade::Sapling,
&[],
None,
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec243_2() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP243_2.zcash_deserialize_into::<Transaction>()?;
let value = hex::decode("adedf02996510200")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&[]);
let input_ind = 1;
let output = transparent::Output { value, lock_script };
let all_previous_outputs = mock_pre_v5_output_list(output, input_ind);
let hasher = SigHasher::new(
&transaction,
HashType::NONE,
NetworkUpgrade::Sapling,
&all_previous_outputs,
Some(input_ind),
);
let hash = hasher.sighash();
let expected = "bbe6d84f57c56b29b914c694baaccb891297e961de3eb46c68e3c89c47b1a1db";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
let lock_script = Script::new(&[]);
let prevout = transparent::Output { value, lock_script };
let index = input_ind;
let all_previous_outputs = mock_pre_v5_output_list(prevout, input_ind);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&transaction,
HashType::NONE,
NetworkUpgrade::Sapling,
&all_previous_outputs,
Some(index),
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec243_3() -> Result<()> {

View File

@ -1,432 +0,0 @@
//! Transparent-related (Bitcoin-inherited) functionality.
use std::{collections::HashMap, fmt, iter};
use crate::{
amount::{Amount, NonNegative},
block,
parameters::Network,
primitives::zcash_primitives,
transaction,
};
mod address;
mod keys;
mod opcodes;
mod script;
mod serialize;
mod utxo;
pub use address::Address;
pub use script::Script;
pub use serialize::{GENESIS_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN};
pub use utxo::{
new_ordered_outputs, new_outputs, outputs_from_utxos, utxos_from_ordered_utxos,
CoinbaseSpendRestriction, OrderedUtxo, Utxo,
};
#[cfg(any(test, feature = "proptest-impl"))]
pub use utxo::{
new_ordered_outputs_with_height, new_outputs_with_height, new_transaction_ordered_outputs,
};
#[cfg(any(test, feature = "proptest-impl"))]
mod arbitrary;
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
/// The maturity threshold for transparent coinbase outputs.
///
/// "A transaction MUST NOT spend a transparent output of a coinbase transaction
/// from a block less than 100 blocks prior to the spend. Note that transparent
/// outputs of coinbase transactions include Founders' Reward outputs and
/// transparent Funding Stream outputs."
/// [7.1](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus)
//
// TODO: change type to HeightDiff
pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
/// Extra coinbase data that identifies some coinbase transactions generated by Zebra.
/// <https://emojipedia.org/zebra/>
//
// # Note
//
// rust-analyzer will crash in some editors when moving over an actual Zebra emoji,
// so we encode it here. This is a known issue in emacs-lsp and other lsp implementations:
// - https://github.com/rust-lang/rust-analyzer/issues/9121
// - https://github.com/emacs-lsp/lsp-mode/issues/2080
// - https://github.com/rust-lang/rust-analyzer/issues/13709
pub const EXTRA_ZEBRA_COINBASE_DATA: &str = "z\u{1F993}";
/// Arbitrary data inserted by miners into a coinbase transaction.
//
// TODO: rename to ExtraCoinbaseData, because height is also part of the coinbase data?
#[derive(Clone, Eq, PartialEq)]
#[cfg_attr(
any(test, feature = "proptest-impl", feature = "elasticsearch"),
derive(Serialize)
)]
pub struct CoinbaseData(
/// Invariant: this vec, together with the coinbase height, must be less than
/// 100 bytes. We enforce this by only constructing CoinbaseData fields by
/// parsing blocks with 100-byte data fields, and checking newly created
/// CoinbaseData lengths in the transaction builder.
pub(super) Vec<u8>,
);
#[cfg(any(test, feature = "proptest-impl"))]
impl CoinbaseData {
/// Create a new `CoinbaseData` containing `data`.
///
/// Only for use in tests.
pub fn new(data: Vec<u8>) -> CoinbaseData {
CoinbaseData(data)
}
}
impl AsRef<[u8]> for CoinbaseData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl std::fmt::Debug for CoinbaseData {
#[allow(clippy::unwrap_in_result)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let escaped = String::from_utf8(
self.0
.iter()
.cloned()
.flat_map(std::ascii::escape_default)
.collect(),
)
.expect("ascii::escape_default produces utf8");
f.debug_tuple("CoinbaseData").field(&escaped).finish()
}
}
/// OutPoint
///
/// A particular transaction output reference.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
#[cfg_attr(
any(test, feature = "proptest-impl", feature = "elasticsearch"),
derive(Serialize)
)]
pub struct OutPoint {
/// References the transaction that contains the UTXO being spent.
///
/// # Correctness
///
/// Consensus-critical serialization uses
/// [`ZcashSerialize`](crate::serialization::ZcashSerialize).
/// [`serde`]-based hex serialization must only be used for testing.
#[cfg_attr(any(test, feature = "proptest-impl"), serde(with = "hex"))]
pub hash: transaction::Hash,
/// Identifies which UTXO from that transaction is referenced; the
/// first output is 0, etc.
pub index: u32,
}
impl OutPoint {
/// Returns a new [`OutPoint`] from an in-memory output `index`.
///
/// # Panics
///
/// If `index` doesn't fit in a [`u32`].
pub fn from_usize(hash: transaction::Hash, index: usize) -> OutPoint {
OutPoint {
hash,
index: index
.try_into()
.expect("valid in-memory output indexes fit in a u32"),
}
}
}
/// A transparent input to a transaction.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
any(test, feature = "proptest-impl", feature = "elasticsearch"),
derive(Serialize)
)]
pub enum Input {
/// A reference to an output of a previous transaction.
PrevOut {
/// The previous output transaction reference.
outpoint: OutPoint,
/// The script that authorizes spending `outpoint`.
unlock_script: Script,
/// The sequence number for the output.
sequence: u32,
},
/// New coins created by the block reward.
Coinbase {
/// The height of this block.
height: block::Height,
/// Free data inserted by miners after the block height.
data: CoinbaseData,
/// The sequence number for the output.
sequence: u32,
},
}
impl fmt::Display for Input {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Input::PrevOut {
outpoint,
unlock_script,
..
} => {
let mut fmter = f.debug_struct("transparent::Input::PrevOut");
fmter.field("unlock_script_len", &unlock_script.as_raw_bytes().len());
fmter.field("outpoint", outpoint);
fmter.finish()
}
Input::Coinbase { height, data, .. } => {
let mut fmter = f.debug_struct("transparent::Input::Coinbase");
fmter.field("height", height);
fmter.field("data_len", &data.0.len());
fmter.finish()
}
}
}
}
impl Input {
/// Returns a new coinbase input for `height` with optional `data` and `sequence`.
///
/// # Consensus
///
/// The combined serialized size of `height` and `data` can be at most 100 bytes.
///
/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
///
/// # Panics
///
/// If the coinbase data is greater than [`MAX_COINBASE_DATA_LEN`].
#[cfg(feature = "getblocktemplate-rpcs")]
pub fn new_coinbase(
height: block::Height,
data: Option<Vec<u8>>,
sequence: Option<u32>,
) -> Input {
// "No extra coinbase data" is the default.
let data = data.unwrap_or_default();
let height_size = height.coinbase_zcash_serialized_size();
assert!(
data.len() + height_size <= MAX_COINBASE_DATA_LEN,
"invalid coinbase data: extra data {} bytes + height {height_size} bytes \
must be {} or less",
data.len(),
MAX_COINBASE_DATA_LEN,
);
Input::Coinbase {
height,
data: CoinbaseData(data),
// If the caller does not specify the sequence number,
// use a sequence number that activates the LockTime.
sequence: sequence.unwrap_or(0),
}
}
/// Returns the extra coinbase data in this input, if it is an [`Input::Coinbase`].
pub fn extra_coinbase_data(&self) -> Option<&CoinbaseData> {
match self {
Input::PrevOut { .. } => None,
Input::Coinbase { data, .. } => Some(data),
}
}
/// Returns the input's sequence number.
pub fn sequence(&self) -> u32 {
match self {
Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => *sequence,
}
}
/// Sets the input's sequence number.
///
/// Only for use in tests.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn set_sequence(&mut self, new_sequence: u32) {
match self {
Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => {
*sequence = new_sequence
}
}
}
/// If this is a [`Input::PrevOut`] input, returns this input's
/// [`OutPoint`]. Otherwise, returns `None`.
pub fn outpoint(&self) -> Option<OutPoint> {
if let Input::PrevOut { outpoint, .. } = self {
Some(*outpoint)
} else {
None
}
}
/// Set this input's [`OutPoint`].
///
/// Should only be called on [`Input::PrevOut`] inputs.
///
/// # Panics
///
/// If `self` is a coinbase input.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn set_outpoint(&mut self, new_outpoint: OutPoint) {
if let Input::PrevOut {
ref mut outpoint, ..
} = self
{
*outpoint = new_outpoint;
} else {
unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints");
}
}
/// Get the value spent by this input, by looking up its [`OutPoint`] in `outputs`.
/// See [`Self::value`] for details.
///
/// # Panics
///
/// If the provided [`Output`]s don't have this input's [`OutPoint`].
pub(crate) fn value_from_outputs(
&self,
outputs: &HashMap<OutPoint, Output>,
) -> Amount<NonNegative> {
match self {
Input::PrevOut { outpoint, .. } => {
outputs
.get(outpoint)
.unwrap_or_else(|| {
panic!(
"provided Outputs (length {:?}) don't have spent {:?}",
outputs.len(),
outpoint
)
})
.value
}
Input::Coinbase { .. } => Amount::zero(),
}
}
/// Get the value spent by this input, by looking up its [`OutPoint`] in
/// [`Utxo`]s.
///
/// This amount is added to the transaction value pool by this input.
///
/// # Panics
///
/// If the provided [`Utxo`]s don't have this input's [`OutPoint`].
pub fn value(&self, utxos: &HashMap<OutPoint, utxo::Utxo>) -> Amount<NonNegative> {
if let Some(outpoint) = self.outpoint() {
// look up the specific Output and convert it to the expected format
let output = utxos
.get(&outpoint)
.expect("provided Utxos don't have spent OutPoint")
.output
.clone();
self.value_from_outputs(&iter::once((outpoint, output)).collect())
} else {
// coinbase inputs don't need any UTXOs
self.value_from_outputs(&HashMap::new())
}
}
/// Get the value spent by this input, by looking up its [`OutPoint`] in
/// [`OrderedUtxo`]s.
///
/// See [`Self::value`] for details.
///
/// # Panics
///
/// If the provided [`OrderedUtxo`]s don't have this input's [`OutPoint`].
pub fn value_from_ordered_utxos(
&self,
ordered_utxos: &HashMap<OutPoint, utxo::OrderedUtxo>,
) -> Amount<NonNegative> {
if let Some(outpoint) = self.outpoint() {
// look up the specific Output and convert it to the expected format
let output = ordered_utxos
.get(&outpoint)
.expect("provided Utxos don't have spent OutPoint")
.utxo
.output
.clone();
self.value_from_outputs(&iter::once((outpoint, output)).collect())
} else {
// coinbase inputs don't need any UTXOs
self.value_from_outputs(&HashMap::new())
}
}
}
/// A transparent output from a transaction.
///
/// The most fundamental building block of a transaction is a
/// transaction output -- the ZEC you own in your "wallet" is in
/// fact a subset of unspent transaction outputs (or "UTXO"s) of the
/// global UTXO set.
///
/// UTXOs are indivisible, discrete units of value which can only be
/// consumed in their entirety. Thus, if I want to send you 1 ZEC and
/// I only own one UTXO worth 2 ZEC, I would construct a transaction
/// that spends my UTXO and sends 1 ZEC to you and 1 ZEC back to me
/// (just like receiving change).
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Deserialize))]
#[cfg_attr(
any(test, feature = "proptest-impl", feature = "elasticsearch"),
derive(Serialize)
)]
pub struct Output {
/// Transaction value.
// At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64.
pub value: Amount<NonNegative>,
/// The lock script defines the conditions under which this output can be spent.
pub lock_script: Script,
}
impl Output {
/// Returns a new coinbase output that pays `amount` using `lock_script`.
#[cfg(feature = "getblocktemplate-rpcs")]
pub fn new_coinbase(amount: Amount<NonNegative>, lock_script: Script) -> Output {
Output {
value: amount,
lock_script,
}
}
/// Get the value contained in this output.
/// This amount is subtracted from the transaction value pool by this output.
pub fn value(&self) -> Amount<NonNegative> {
self.value
}
/// Return the destination address from a transparent output.
///
/// Returns None if the address type is not valid or unrecognized.
pub fn address(&self, network: &Network) -> Option<Address> {
zcash_primitives::transparent_output_address(self, network)
}
}

View File

@ -1,361 +0,0 @@
//! Transparent Address types.
use std::{fmt, io};
use crate::{
parameters::NetworkKind,
serialization::{SerializationError, ZcashDeserialize, ZcashSerialize},
transparent::{opcodes::OpCode, Script},
};
#[cfg(test)]
use proptest::prelude::*;
/// Transparent Zcash Addresses
///
/// In Bitcoin a single byte is used for the version field identifying
/// the address type. In Zcash two bytes are used. For addresses on
/// the production network, this and the encoded length cause the first
/// two characters of the Base58Check encoding to be fixed as "t3" for
/// P2SH addresses, and as "t1" for P2PKH addresses. (This does not
/// imply that a transparent Zcash address can be parsed identically
/// to a Bitcoin address just by removing the "t".)
///
/// <https://zips.z.cash/protocol/protocol.pdf#transparentaddrencoding>
#[derive(
Clone, Eq, PartialEq, Hash, serde_with::SerializeDisplay, serde_with::DeserializeFromStr,
)]
pub enum Address {
/// P2SH (Pay to Script Hash) addresses
PayToScriptHash {
/// Production, test, or other network
network_kind: NetworkKind,
/// 20 bytes specifying a script hash.
script_hash: [u8; 20],
},
/// P2PKH (Pay to Public Key Hash) addresses
PayToPublicKeyHash {
/// Production, test, or other network
network_kind: NetworkKind,
/// 20 bytes specifying a public key hash, which is a RIPEMD-160
/// hash of a SHA-256 hash of a compressed ECDSA key encoding.
pub_key_hash: [u8; 20],
},
}
impl fmt::Debug for Address {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut debug_struct = f.debug_struct("TransparentAddress");
match self {
Address::PayToScriptHash {
network_kind,
script_hash,
} => debug_struct
.field("network_kind", network_kind)
.field("script_hash", &hex::encode(script_hash))
.finish(),
Address::PayToPublicKeyHash {
network_kind,
pub_key_hash,
} => debug_struct
.field("network_kind", network_kind)
.field("pub_key_hash", &hex::encode(pub_key_hash))
.finish(),
}
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut bytes = io::Cursor::new(Vec::new());
let _ = self.zcash_serialize(&mut bytes);
f.write_str(&bs58::encode(bytes.get_ref()).with_check().into_string())
}
}
impl std::str::FromStr for Address {
type Err = SerializationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let result = &bs58::decode(s).with_check(None).into_vec();
match result {
Ok(bytes) => Self::zcash_deserialize(&bytes[..]),
Err(_) => Err(SerializationError::Parse("t-addr decoding error")),
}
}
}
impl ZcashSerialize for Address {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
Address::PayToScriptHash {
network_kind,
script_hash,
} => {
writer.write_all(&network_kind.b58_script_address_prefix())?;
writer.write_all(script_hash)?
}
Address::PayToPublicKeyHash {
network_kind,
pub_key_hash,
} => {
writer.write_all(&network_kind.b58_pubkey_address_prefix())?;
writer.write_all(pub_key_hash)?
}
}
Ok(())
}
}
impl ZcashDeserialize for Address {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let mut version_bytes = [0; 2];
reader.read_exact(&mut version_bytes)?;
let mut hash_bytes = [0; 20];
reader.read_exact(&mut hash_bytes)?;
match version_bytes {
zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX => {
Ok(Address::PayToScriptHash {
network_kind: NetworkKind::Mainnet,
script_hash: hash_bytes,
})
}
zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX => {
Ok(Address::PayToScriptHash {
network_kind: NetworkKind::Testnet,
script_hash: hash_bytes,
})
}
zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX => {
Ok(Address::PayToPublicKeyHash {
network_kind: NetworkKind::Mainnet,
pub_key_hash: hash_bytes,
})
}
zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX => {
Ok(Address::PayToPublicKeyHash {
network_kind: NetworkKind::Testnet,
pub_key_hash: hash_bytes,
})
}
_ => Err(SerializationError::Parse("bad t-addr version/type")),
}
}
}
impl Address {
/// Create an address for the given public key hash and network.
pub fn from_pub_key_hash(network_kind: NetworkKind, pub_key_hash: [u8; 20]) -> Self {
Self::PayToPublicKeyHash {
network_kind,
pub_key_hash,
}
}
/// Create an address for the given script hash and network.
pub fn from_script_hash(network_kind: NetworkKind, script_hash: [u8; 20]) -> Self {
Self::PayToScriptHash {
network_kind,
script_hash,
}
}
/// Returns the network kind for this address.
pub fn network_kind(&self) -> NetworkKind {
match self {
Address::PayToScriptHash { network_kind, .. } => *network_kind,
Address::PayToPublicKeyHash { network_kind, .. } => *network_kind,
}
}
/// Returns `true` if the address is `PayToScriptHash`, and `false` if it is `PayToPublicKeyHash`.
pub fn is_script_hash(&self) -> bool {
matches!(self, Address::PayToScriptHash { .. })
}
/// Returns the hash bytes for this address, regardless of the address type.
///
/// # Correctness
///
/// Use [`ZcashSerialize`] and [`ZcashDeserialize`] for consensus-critical serialization.
pub fn hash_bytes(&self) -> [u8; 20] {
match *self {
Address::PayToScriptHash { script_hash, .. } => script_hash,
Address::PayToPublicKeyHash { pub_key_hash, .. } => pub_key_hash,
}
}
/// Given a transparent address (P2SH or a P2PKH), create a script that can be used in a coinbase
/// transaction output.
pub fn create_script_from_address(&self) -> Script {
let mut script_bytes = Vec::new();
match self {
// https://developer.bitcoin.org/devguide/transactions.html#pay-to-script-hash-p2sh
Address::PayToScriptHash { .. } => {
script_bytes.push(OpCode::Hash160 as u8);
script_bytes.push(OpCode::Push20Bytes as u8);
script_bytes.extend(self.hash_bytes());
script_bytes.push(OpCode::Equal as u8);
}
// https://developer.bitcoin.org/devguide/transactions.html#pay-to-public-key-hash-p2pkh
Address::PayToPublicKeyHash { .. } => {
script_bytes.push(OpCode::Dup as u8);
script_bytes.push(OpCode::Hash160 as u8);
script_bytes.push(OpCode::Push20Bytes as u8);
script_bytes.extend(self.hash_bytes());
script_bytes.push(OpCode::EqualVerify as u8);
script_bytes.push(OpCode::CheckSig as u8);
}
};
Script::new(&script_bytes)
}
}
#[cfg(test)]
mod tests {
use ripemd::{Digest, Ripemd160};
use secp256k1::PublicKey;
use sha2::Sha256;
use super::*;
trait ToAddressWithNetwork {
/// Convert `self` to an `Address`, given the current `network`.
fn to_address(&self, network: NetworkKind) -> Address;
}
impl ToAddressWithNetwork for Script {
fn to_address(&self, network_kind: NetworkKind) -> Address {
Address::PayToScriptHash {
network_kind,
script_hash: Address::hash_payload(self.as_raw_bytes()),
}
}
}
impl ToAddressWithNetwork for PublicKey {
fn to_address(&self, network_kind: NetworkKind) -> Address {
Address::PayToPublicKeyHash {
network_kind,
pub_key_hash: Address::hash_payload(&self.serialize()[..]),
}
}
}
impl Address {
/// A hash of a transparent address payload, as used in
/// transparent pay-to-script-hash and pay-to-publickey-hash
/// addresses.
///
/// The resulting hash in both of these cases is always exactly 20
/// bytes.
/// <https://en.bitcoin.it/Base58Check_encoding#Encoding_a_Bitcoin_address>
#[allow(dead_code)]
fn hash_payload(bytes: &[u8]) -> [u8; 20] {
let sha_hash = Sha256::digest(bytes);
let ripe_hash = Ripemd160::digest(sha_hash);
let mut payload = [0u8; 20];
payload[..].copy_from_slice(&ripe_hash[..]);
payload
}
}
#[test]
fn pubkey_mainnet() {
let _init_guard = zebra_test::init();
let pub_key = PublicKey::from_slice(&[
3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
])
.expect("A PublicKey from slice");
let t_addr = pub_key.to_address(NetworkKind::Mainnet);
assert_eq!(format!("{t_addr}"), "t1bmMa1wJDFdbc2TiURQP5BbBz6jHjUBuHq");
}
#[test]
fn pubkey_testnet() {
let _init_guard = zebra_test::init();
let pub_key = PublicKey::from_slice(&[
3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
])
.expect("A PublicKey from slice");
let t_addr = pub_key.to_address(NetworkKind::Testnet);
assert_eq!(format!("{t_addr}"), "tmTc6trRhbv96kGfA99i7vrFwb5p7BVFwc3");
}
#[test]
fn empty_script_mainnet() {
let _init_guard = zebra_test::init();
let script = Script::new(&[0u8; 20]);
let t_addr = script.to_address(NetworkKind::Mainnet);
assert_eq!(format!("{t_addr}"), "t3Y5pHwfgHbS6pDjj1HLuMFxhFFip1fcJ6g");
}
#[test]
fn empty_script_testnet() {
let _init_guard = zebra_test::init();
let script = Script::new(&[0; 20]);
let t_addr = script.to_address(NetworkKind::Testnet);
assert_eq!(format!("{t_addr}"), "t2L51LcmpA43UMvKTw2Lwtt9LMjwyqU2V1P");
}
#[test]
fn from_string() {
let _init_guard = zebra_test::init();
let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
assert_eq!(format!("{t_addr}"), "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd");
}
#[test]
fn debug() {
let _init_guard = zebra_test::init();
let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
assert_eq!(
format!("{t_addr:?}"),
"TransparentAddress { network_kind: Mainnet, script_hash: \"7d46a730d31f97b1930d3368a967c309bd4d136a\" }"
);
}
}
#[cfg(test)]
proptest! {
#[test]
fn transparent_address_roundtrip(taddr in any::<Address>()) {
let _init_guard = zebra_test::init();
let mut data = Vec::new();
taddr.zcash_serialize(&mut data).expect("t-addr should serialize");
let taddr2 = Address::zcash_deserialize(&data[..]).expect("randomized t-addr should deserialize");
prop_assert_eq![taddr, taddr2];
}
}

View File

@ -1,72 +0,0 @@
use proptest::{collection::vec, prelude::*};
use crate::{block, parameters::NetworkKind, LedgerState};
use super::{Address, CoinbaseData, Input, OutPoint, Script, GENESIS_COINBASE_DATA};
impl Input {
/// Construct a strategy for creating valid-ish vecs of Inputs.
pub fn vec_strategy(ledger_state: &LedgerState, max_size: usize) -> BoxedStrategy<Vec<Self>> {
if ledger_state.has_coinbase {
Self::arbitrary_with(Some(ledger_state.height))
.prop_map(|input| vec![input])
.boxed()
} else {
vec(Self::arbitrary_with(None), 1..=max_size).boxed()
}
}
}
impl Arbitrary for Input {
type Parameters = Option<block::Height>;
fn arbitrary_with(height: Self::Parameters) -> Self::Strategy {
if let Some(height) = height {
(vec(any::<u8>(), 0..95), any::<u32>())
.prop_map(move |(data, sequence)| Input::Coinbase {
height,
data: if height == block::Height(0) {
CoinbaseData(GENESIS_COINBASE_DATA.to_vec())
} else {
CoinbaseData(data)
},
sequence,
})
.boxed()
} else {
(any::<OutPoint>(), any::<Script>(), any::<u32>())
.prop_map(|(outpoint, unlock_script, sequence)| Input::PrevOut {
outpoint,
unlock_script,
sequence,
})
.boxed()
}
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Address {
type Parameters = ();
fn arbitrary_with(_args: ()) -> Self::Strategy {
any::<(bool, bool, [u8; 20])>()
.prop_map(|(is_mainnet, is_p2pkh, hash_bytes)| {
let network = if is_mainnet {
NetworkKind::Mainnet
} else {
NetworkKind::Testnet
};
if is_p2pkh {
Address::from_pub_key_hash(network, hash_bytes)
} else {
Address::from_script_hash(network, hash_bytes)
}
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}

View File

@ -1,26 +0,0 @@
//! Transparent key trait impls, around secp256k1::PublicKey
//!
//! We don't impl Arbitrary for PublicKey since it's being pulled in
//! from secp256k1 and we don't want to wrap it.
use std::io;
use secp256k1::PublicKey;
use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize};
impl ZcashSerialize for PublicKey {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.serialize())?;
Ok(())
}
}
impl ZcashDeserialize for PublicKey {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let mut bytes = [0; 33];
reader.read_exact(&mut bytes[..])?;
Self::from_slice(&bytes[..])
.map_err(|_| SerializationError::Parse("invalid secp256k1 compressed public key"))
}
}

View File

@ -1,15 +0,0 @@
//! Zebra script opcodes.
/// Supported opcodes
///
/// <https://github.com/zcash/zcash/blob/8b16094f6672d8268ff25b2d7bddd6a6207873f7/src/script/script.h#L39>
pub enum OpCode {
// Opcodes used to generate P2SH scripts.
Equal = 0x87,
Hash160 = 0xa9,
Push20Bytes = 0x14,
// Additional opcodes used to generate P2PKH scripts.
Dup = 0x76,
EqualVerify = 0x88,
CheckSig = 0xac,
}

View File

@ -1,113 +0,0 @@
//! Bitcoin script for Zebra
use std::{fmt, io};
use hex::ToHex;
use crate::serialization::{
zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashSerialize,
};
/// An encoding of a Bitcoin script.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[cfg_attr(
any(test, feature = "proptest-impl"),
derive(proptest_derive::Arbitrary)
)]
pub struct Script(
/// # Correctness
///
/// Consensus-critical serialization uses [`ZcashSerialize`].
/// [`serde`]-based hex serialization must only be used for RPCs and testing.
#[serde(with = "hex")]
Vec<u8>,
);
impl Script {
/// Create a new Bitcoin script from its raw bytes.
/// The raw bytes must not contain the length prefix.
pub fn new(raw_bytes: &[u8]) -> Self {
Script(raw_bytes.to_vec())
}
/// Return the raw bytes of the script without the length prefix.
///
/// # Correctness
///
/// These raw bytes do not have a length prefix.
/// The Zcash serialization format requires a length prefix; use `zcash_serialize`
/// and `zcash_deserialize` to create byte data with a length prefix.
pub fn as_raw_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Display for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.encode_hex::<String>())
}
}
impl fmt::Debug for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Script")
.field(&hex::encode(&self.0))
.finish()
}
}
impl ToHex for &Script {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.as_raw_bytes().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.as_raw_bytes().encode_hex_upper()
}
}
impl ToHex for Script {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl ZcashSerialize for Script {
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
zcash_serialize_bytes(&self.0, writer)
}
}
impl ZcashDeserialize for Script {
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError> {
Vec::zcash_deserialize(reader).map(Script)
}
}
#[cfg(test)]
mod proptests {
use std::io::Cursor;
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
fn script_roundtrip(script in any::<Script>()) {
let _init_guard = zebra_test::init();
let mut bytes = Cursor::new(Vec::new());
script.zcash_serialize(&mut bytes)?;
bytes.set_position(0);
let other_script = Script::zcash_deserialize(&mut bytes)?;
prop_assert_eq![script, other_script];
}
}
}

View File

@ -1,349 +0,0 @@
//! Serializes and deserializes transparent data.
use std::io;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::{
block::{self, Height},
serialization::{
zcash_serialize_bytes, FakeWriter, ReadZcashExt, SerializationError, ZcashDeserialize,
ZcashDeserializeInto, ZcashSerialize,
},
transaction,
};
use super::{CoinbaseData, Input, OutPoint, Output, Script};
/// The maximum length of the coinbase data.
///
/// Includes the encoded coinbase height, if any.
///
/// # Consensus
///
/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
pub const MAX_COINBASE_DATA_LEN: usize = 100;
/// The maximum length of the encoded coinbase height.
///
/// # Consensus
///
/// > The length of heightBytes MUST be in the range {1 .. 5}. Then the encoding is the length
/// > of heightBytes encoded as one byte, followed by heightBytes itself.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
pub const MAX_COINBASE_HEIGHT_DATA_LEN: usize = 6;
/// The minimum length of the coinbase data.
///
/// Includes the encoded coinbase height, if any.
///
/// # Consensus
///
/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
pub const MIN_COINBASE_DATA_LEN: usize = 2;
/// The coinbase data for a genesis block.
///
/// Zcash uses the same coinbase data for the Mainnet, Testnet, and Regtest
/// genesis blocks.
pub const GENESIS_COINBASE_DATA: [u8; 77] = [
4, 255, 255, 7, 31, 1, 4, 69, 90, 99, 97, 115, 104, 48, 98, 57, 99, 52, 101, 101, 102, 56, 98,
55, 99, 99, 52, 49, 55, 101, 101, 53, 48, 48, 49, 101, 51, 53, 48, 48, 57, 56, 52, 98, 54, 102,
101, 97, 51, 53, 54, 56, 51, 97, 55, 99, 97, 99, 49, 52, 49, 97, 48, 52, 51, 99, 52, 50, 48,
54, 52, 56, 51, 53, 100, 51, 52,
];
impl ZcashSerialize for OutPoint {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.hash.0[..])?;
writer.write_u32::<LittleEndian>(self.index)?;
Ok(())
}
}
impl ZcashDeserialize for OutPoint {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
Ok(OutPoint {
hash: transaction::Hash(reader.read_32_bytes()?),
index: reader.read_u32::<LittleEndian>()?,
})
}
}
// Coinbase inputs include block heights (BIP34). These are not encoded
// directly, but as a Bitcoin script that pushes the block height to the stack
// when executed. The script data is otherwise unused. Because we want to
// *parse* transactions into an internal representation where illegal states are
// unrepresentable, we need just enough parsing of Bitcoin scripts to parse the
// coinbase height and split off the rest of the (inert) coinbase data.
// Starting at Network Upgrade 5, coinbase transactions also encode the block
// height in the expiry height field. But Zebra does not use this field to
// determine the coinbase height, because it is not present in older network
// upgrades.
/// Split `data` into a block height and remaining miner-controlled coinbase data.
///
/// The height may consume `0..=5` bytes at the stat of the coinbase data.
/// The genesis block does not include an encoded coinbase height.
///
/// # Consensus
///
/// > A coinbase transaction for a *block* at *block height* greater than 0 MUST have
/// > a script that, as its first item, encodes the *block height* `height` as follows.
/// > For `height` in the range {1..16}, the encoding is a single byte of value
/// > `0x50` + `height`. Otherwise, let `heightBytes` be the signed little-endian
/// > representation of `height`, using the minimum nonzero number of bytes such that
/// > the most significant byte is < `0x80`.
/// > The length of `heightBytes` MUST be in the range {1..5}.
/// > Then the encoding is the length of `heightBytes` encoded as one byte,
/// > followed by `heightBytes` itself. This matches the encoding used by Bitcoin in the
/// > implementation of [BIP-34] (but the description here is to be considered normative).
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
/// <https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki>
pub(crate) fn parse_coinbase_height(
mut data: Vec<u8>,
) -> Result<(block::Height, CoinbaseData), SerializationError> {
match (data.first(), data.len()) {
// Blocks 1 through 16 inclusive encode block height with OP_N opcodes.
(Some(op_n @ 0x51..=0x60), len) if len >= 1 => Ok((
Height((op_n - 0x50) as u32),
CoinbaseData(data.split_off(1)),
)),
// Blocks 17 through 128 exclusive encode block height with the `0x01` opcode.
// The Bitcoin encoding requires that the most significant byte is below 0x80.
(Some(0x01), len) if len >= 2 && data[1] < 0x80 => {
let h = data[1] as u32;
if (17..128).contains(&h) {
Ok((Height(h), CoinbaseData(data.split_off(2))))
} else {
Err(SerializationError::Parse("Invalid block height"))
}
}
// Blocks 128 through 32768 exclusive encode block height with the `0x02` opcode.
// The Bitcoin encoding requires that the most significant byte is below 0x80.
(Some(0x02), len) if len >= 3 && data[2] < 0x80 => {
let h = data[1] as u32 + ((data[2] as u32) << 8);
if (128..32_768).contains(&h) {
Ok((Height(h), CoinbaseData(data.split_off(3))))
} else {
Err(SerializationError::Parse("Invalid block height"))
}
}
// Blocks 32768 through 2**23 exclusive encode block height with the `0x03` opcode.
// The Bitcoin encoding requires that the most significant byte is below 0x80.
(Some(0x03), len) if len >= 4 && data[3] < 0x80 => {
let h = data[1] as u32 + ((data[2] as u32) << 8) + ((data[3] as u32) << 16);
if (32_768..8_388_608).contains(&h) {
Ok((Height(h), CoinbaseData(data.split_off(4))))
} else {
Err(SerializationError::Parse("Invalid block height"))
}
}
// The genesis block does not encode the block height by mistake; special case it.
// The first five bytes are [4, 255, 255, 7, 31], the little-endian encoding of
// 520_617_983.
//
// In the far future, Zcash might reach this height, and the miner might use the
// same coinbase data as the genesis block. So we need an updated consensus rule
// to handle this edge case.
//
// TODO: update this check based on the consensus rule changes in
// https://github.com/zcash/zips/issues/540
(Some(0x04), _) if data[..] == GENESIS_COINBASE_DATA[..] => {
Ok((Height(0), CoinbaseData(data)))
}
// As noted above, this is included for completeness.
// The Bitcoin encoding requires that the most significant byte is below 0x80.
(Some(0x04), len) if len >= 5 && data[4] < 0x80 => {
let h = data[1] as u32
+ ((data[2] as u32) << 8)
+ ((data[3] as u32) << 16)
+ ((data[4] as u32) << 24);
if (8_388_608..=Height::MAX.0).contains(&h) {
Ok((Height(h), CoinbaseData(data.split_off(5))))
} else {
Err(SerializationError::Parse("Invalid block height"))
}
}
_ => Err(SerializationError::Parse(
"Could not parse BIP34 height in coinbase data",
)),
}
}
/// Encode `height` into a block height, as a prefix of the coinbase data.
/// Does not write `coinbase_data`.
///
/// The height may produce `0..=5` initial bytes of coinbase data.
///
/// # Errors
///
/// Returns an error if the coinbase height is zero,
/// and the `coinbase_data` does not match the Zcash mainnet and testnet genesis coinbase data.
/// (They are identical.)
///
/// This check is required, because the genesis block does not include an encoded
/// coinbase height,
pub(crate) fn write_coinbase_height<W: io::Write>(
height: block::Height,
coinbase_data: &CoinbaseData,
mut w: W,
) -> Result<(), io::Error> {
// We can't write this as a match statement on stable until exclusive range
// guards are stabilized.
// The Bitcoin encoding requires that the most significant byte is below 0x80,
// so the ranges run up to 2^{n-1} rather than 2^n.
if let 0 = height.0 {
// The genesis block's coinbase data does not have a height prefix.
// So we return an error if the entire coinbase data doesn't match genesis.
// (If we don't do this check, then deserialization will fail.)
//
// TODO: update this check based on the consensus rule changes in
// https://github.com/zcash/zips/issues/540
if coinbase_data.0 != GENESIS_COINBASE_DATA {
return Err(io::Error::new(
io::ErrorKind::Other,
"invalid genesis coinbase data",
));
}
} else if let h @ 1..=16 = height.0 {
w.write_u8(0x50 + (h as u8))?;
} else if let h @ 17..=127 = height.0 {
w.write_u8(0x01)?;
w.write_u8(h as u8)?;
} else if let h @ 128..=32_767 = height.0 {
w.write_u8(0x02)?;
w.write_u16::<LittleEndian>(h as u16)?;
} else if let h @ 32_768..=8_388_607 = height.0 {
w.write_u8(0x03)?;
w.write_u8(h as u8)?;
w.write_u8((h >> 8) as u8)?;
w.write_u8((h >> 16) as u8)?;
} else if let h @ 8_388_608..=block::Height::MAX_AS_U32 = height.0 {
w.write_u8(0x04)?;
w.write_u32::<LittleEndian>(h)?;
} else {
panic!("Invalid coinbase height");
}
Ok(())
}
impl Height {
/// Get the size of `Height` when serialized into a coinbase input script.
pub fn coinbase_zcash_serialized_size(&self) -> usize {
let mut writer = FakeWriter(0);
let empty_data = CoinbaseData(Vec::new());
write_coinbase_height(*self, &empty_data, &mut writer).expect("writer should never fail");
writer.0
}
}
impl ZcashSerialize for Input {
/// Serialize this transparent input.
///
/// # Errors
///
/// Returns an error if the coinbase height is zero,
/// and the coinbase data does not match the Zcash mainnet and testnet genesis coinbase data.
/// (They are identical.)
///
/// This check is required, because the genesis block does not include an encoded
/// coinbase height,
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
Input::PrevOut {
outpoint,
unlock_script,
sequence,
} => {
outpoint.zcash_serialize(&mut writer)?;
unlock_script.zcash_serialize(&mut writer)?;
writer.write_u32::<LittleEndian>(*sequence)?;
}
Input::Coinbase {
height,
data,
sequence,
} => {
writer.write_all(&[0; 32][..])?;
writer.write_u32::<LittleEndian>(0xffff_ffff)?;
let mut height_and_data = Vec::new();
write_coinbase_height(*height, data, &mut height_and_data)?;
height_and_data.extend(&data.0);
zcash_serialize_bytes(&height_and_data, &mut writer)?;
writer.write_u32::<LittleEndian>(*sequence)?;
}
}
Ok(())
}
}
impl ZcashDeserialize for Input {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// This inlines the OutPoint deserialization to peek at the hash value
// and detect whether we have a coinbase input.
let bytes = reader.read_32_bytes()?;
if bytes == [0; 32] {
if reader.read_u32::<LittleEndian>()? != 0xffff_ffff {
return Err(SerializationError::Parse("wrong index in coinbase"));
}
let data: Vec<u8> = (&mut reader).zcash_deserialize_into()?;
// Check the coinbase data length.
if data.len() > MAX_COINBASE_DATA_LEN {
return Err(SerializationError::Parse("coinbase data is too long"));
} else if data.len() < MIN_COINBASE_DATA_LEN {
return Err(SerializationError::Parse("coinbase data is too short"));
}
let (height, data) = parse_coinbase_height(data)?;
let sequence = reader.read_u32::<LittleEndian>()?;
Ok(Input::Coinbase {
height,
data,
sequence,
})
} else {
Ok(Input::PrevOut {
outpoint: OutPoint {
hash: transaction::Hash(bytes),
index: reader.read_u32::<LittleEndian>()?,
},
unlock_script: Script::zcash_deserialize(&mut reader)?,
sequence: reader.read_u32::<LittleEndian>()?,
})
}
}
}
impl ZcashSerialize for Output {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
self.value.zcash_serialize(&mut writer)?;
self.lock_script.zcash_serialize(&mut writer)?;
Ok(())
}
}
impl ZcashDeserialize for Output {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let reader = &mut reader;
Ok(Output {
value: reader.zcash_deserialize_into()?,
lock_script: Script::zcash_deserialize(reader)?,
})
}
}

View File

@ -1,2 +0,0 @@
mod prop;
mod vectors;

View File

@ -1,100 +0,0 @@
//! Property tests for transparent inputs and outputs.
use zebra_test::prelude::*;
use crate::{block, fmt::SummaryDebug, transaction::arbitrary::MAX_ARBITRARY_ITEMS, LedgerState};
use super::super::{
serialize::{parse_coinbase_height, write_coinbase_height},
Input,
};
use proptest::collection::vec;
#[test]
fn coinbase_has_height() -> Result<()> {
let _init_guard = zebra_test::init();
let strategy =
any::<block::Height>().prop_flat_map(|height| Input::arbitrary_with(Some(height)));
proptest!(|(input in strategy)| {
let is_coinbase = matches!(input, Input::Coinbase { .. });
prop_assert!(is_coinbase);
});
Ok(())
}
#[test]
fn input_coinbase_vecs_only_have_coinbase_input() -> Result<()> {
let _init_guard = zebra_test::init();
let strategy = LedgerState::coinbase_strategy(None, None, false)
.prop_flat_map(|ledger_state| Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS));
proptest!(|(inputs in strategy.prop_map(SummaryDebug))| {
let len = inputs.len();
for (ind, input) in inputs.into_iter().enumerate() {
let is_coinbase = matches!(input, Input::Coinbase { .. });
if ind == 0 {
prop_assert!(is_coinbase);
prop_assert_eq!(1, len);
} else {
prop_assert!(!is_coinbase);
}
}
});
Ok(())
}
#[test]
fn coinbase_height_round_trip_from_random_input() -> Result<()> {
let _init_guard = zebra_test::init();
let strategy =
any::<block::Height>().prop_flat_map(|height| Input::arbitrary_with(Some(height)));
proptest!(|(input in strategy)| {
let (height, data) = match input {
Input::Coinbase { height, data, .. } => (height, data),
_ => unreachable!("all inputs will have coinbase height and data"),
};
let mut encoded = Vec::new();
write_coinbase_height(height, &data, &mut encoded)?;
let decoded = parse_coinbase_height(encoded)?;
prop_assert_eq!(height, decoded.0);
});
Ok(())
}
proptest! {
#[test]
fn coinbase_height_round_trip_from_random_bytes(mut height_bytes in vec(any::<u8>(), 1..5)) {
let mut encoded1 = vec![height_bytes.len() as u8];
encoded1.append(&mut height_bytes);
let decoded = parse_coinbase_height(encoded1.clone()).ok();
if decoded.is_some() {
let mut encoded2 = Vec::new();
write_coinbase_height(decoded.as_ref().unwrap().0, &decoded.unwrap().1, &mut encoded2)?;
prop_assert_eq!(encoded2, encoded1);
}
}
#[test]
fn coinbase_height_round_trip_from_random_byte(height_byte in vec(any::<u8>(), 1..2)) {
let encoded1 = height_byte;
let decoded = parse_coinbase_height(encoded1.clone()).ok();
if decoded.is_some() {
let mut encoded2 = Vec::new();
write_coinbase_height(decoded.as_ref().unwrap().0, &decoded.unwrap().1, &mut encoded2)?;
prop_assert_eq!(encoded2, encoded1);
}
}
}

View File

@ -1,124 +0,0 @@
use std::sync::Arc;
use super::super::serialize::parse_coinbase_height;
use crate::{
block::Block, parameters::Network, primitives::zcash_primitives::transparent_output_address,
serialization::ZcashDeserializeInto, transaction,
};
use hex::FromHex;
use zebra_test::prelude::*;
#[test]
fn parse_coinbase_height_mins() {
let _init_guard = zebra_test::init();
// examples with height 1:
let case1 = vec![0x51];
assert!(parse_coinbase_height(case1.clone()).is_ok());
assert_eq!(parse_coinbase_height(case1).unwrap().0 .0, 1);
let case2 = vec![0x01, 0x01];
assert!(parse_coinbase_height(case2).is_err());
let case3 = vec![0x02, 0x01, 0x00];
assert!(parse_coinbase_height(case3).is_err());
let case4 = vec![0x03, 0x01, 0x00, 0x00];
assert!(parse_coinbase_height(case4).is_err());
let case5 = vec![0x04, 0x01, 0x00, 0x00, 0x00];
assert!(parse_coinbase_height(case5).is_err());
// examples with height 17:
let case1 = vec![0x01, 0x11];
assert!(parse_coinbase_height(case1.clone()).is_ok());
assert_eq!(parse_coinbase_height(case1).unwrap().0 .0, 17);
let case2 = vec![0x02, 0x11, 0x00];
assert!(parse_coinbase_height(case2).is_err());
let case3 = vec![0x03, 0x11, 0x00, 0x00];
assert!(parse_coinbase_height(case3).is_err());
let case4 = vec![0x04, 0x11, 0x00, 0x00, 0x00];
assert!(parse_coinbase_height(case4).is_err());
}
#[test]
fn get_transparent_output_address() -> Result<()> {
let _init_guard = zebra_test::init();
let script_tx: Vec<u8> = <Vec<u8>>::from_hex("0400008085202f8901fcaf44919d4a17f6181a02a7ebe0420be6f7dad1ef86755b81d5a9567456653c010000006a473044022035224ed7276e61affd53315eca059c92876bc2df61d84277cafd7af61d4dbf4002203ed72ea497a9f6b38eb29df08e830d99e32377edb8a574b8a289024f0241d7c40121031f54b095eae066d96b2557c1f99e40e967978a5fd117465dbec0986ca74201a6feffffff020050d6dc0100000017a9141b8a9bda4b62cd0d0582b55455d0778c86f8628f870d03c812030000001976a914e4ff5512ffafe9287992a1cd177ca6e408e0300388ac62070d0095070d000000000000000000000000")
.expect("Block bytes are in valid hex representation");
let transaction = script_tx.zcash_deserialize_into::<Arc<transaction::Transaction>>()?;
// Hashes were extracted from the transaction (parsed with zebra-chain,
// then manually extracted from lock_script).
// Final expected values were generated with https://secretscan.org/PrivateKeyHex,
// by filling field 4 with the prefix followed by the address hash.
// Refer to <https://zips.z.cash/protocol/protocol.pdf#transparentaddrencoding>
// for the prefixes.
// Script hash 1b8a9bda4b62cd0d0582b55455d0778c86f8628f
let addr = transparent_output_address(&transaction.outputs()[0], &Network::Mainnet)
.expect("should return address");
assert_eq!(addr.to_string(), "t3M5FDmPfWNRG3HRLddbicsuSCvKuk9hxzZ");
let addr =
transparent_output_address(&transaction.outputs()[0], &Network::new_default_testnet())
.expect("should return address");
assert_eq!(addr.to_string(), "t294SGSVoNq2daz15ZNbmAW65KQZ5e3nN5G");
// Public key hash e4ff5512ffafe9287992a1cd177ca6e408e03003
let addr = transparent_output_address(&transaction.outputs()[1], &Network::Mainnet)
.expect("should return address");
assert_eq!(addr.to_string(), "t1ekRwsd4LaSsd6NXgsx66q2HxQWTLCF44y");
let addr =
transparent_output_address(&transaction.outputs()[1], &Network::new_default_testnet())
.expect("should return address");
assert_eq!(addr.to_string(), "tmWbBGi7TjExNmLZyMcFpxVh3ZPbGrpbX3H");
Ok(())
}
#[test]
fn get_transparent_output_address_with_blocks() {
let _init_guard = zebra_test::init();
for network in Network::iter() {
get_transparent_output_address_with_blocks_for_network(network);
}
}
/// Test that the block test vector indexes match the heights in the block data,
/// and that each post-sapling block has a corresponding final sapling root.
fn get_transparent_output_address_with_blocks_for_network(network: Network) {
let block_iter = network.block_iter();
let mut valid_addresses = 0;
for (&height, block_bytes) in block_iter.skip(1) {
let block = block_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
for (idx, tx) in block.transactions.iter().enumerate() {
for output in tx.outputs() {
let addr = output.address(&network);
if addr.is_none() && idx == 0 && output.lock_script.as_raw_bytes()[0] == 0x21 {
// There are a bunch of coinbase transactions with pay-to-pubkey scripts
// which we don't support; skip them
continue;
}
assert!(
addr.is_some(),
"address of {output:?}; block #{height}; tx #{idx}; must not be None",
);
valid_addresses += 1;
}
}
}
// Make sure we didn't accidentally skip all vectors
assert!(valid_addresses > 0);
}

View File

@ -1,252 +0,0 @@
//! Unspent transparent output data structures and functions.
use std::collections::HashMap;
use crate::{
block::{self, Block, Height},
transaction::{self, Transaction},
transparent,
};
/// An unspent `transparent::Output`, with accompanying metadata.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "proptest-impl"),
derive(proptest_derive::Arbitrary, serde::Serialize)
)]
pub struct Utxo {
/// The output itself.
pub output: transparent::Output,
// TODO: replace the height and from_coinbase fields with OutputLocation,
// and provide lookup/calculation methods for height and from_coinbase
//
/// The height at which the output was created.
pub height: block::Height,
/// Whether the output originated in a coinbase transaction.
pub from_coinbase: bool,
}
/// A [`Utxo`], and the index of its transaction within its block.
///
/// This extra index is used to check that spends come after outputs,
/// when a new output and its spend are both in the same block.
///
/// The extra index is only used during block verification,
/// so it does not need to be sent to the state.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "proptest-impl"),
derive(proptest_derive::Arbitrary)
)]
//
// TODO: after modifying UTXO to contain an OutputLocation, replace this type with UTXO
pub struct OrderedUtxo {
/// An unspent transaction output.
pub utxo: Utxo,
/// The index of the transaction that created the output, in the block at `height`.
///
/// Used to make sure that transaction can only spend outputs
/// that were created earlier in the chain.
///
/// Note: this is different from `OutPoint.index`,
/// which is the index of the output in its transaction.
pub tx_index_in_block: usize,
}
impl AsRef<Utxo> for OrderedUtxo {
fn as_ref(&self) -> &Utxo {
&self.utxo
}
}
impl Utxo {
/// Create a new UTXO from its fields.
pub fn new(output: transparent::Output, height: block::Height, from_coinbase: bool) -> Utxo {
Utxo {
output,
height,
from_coinbase,
}
}
/// Create a new UTXO from an output and its transaction location.
pub fn from_location(
output: transparent::Output,
height: block::Height,
tx_index_in_block: usize,
) -> Utxo {
// Coinbase transactions are always the first transaction in their block,
// we check the other consensus rules separately.
let from_coinbase = tx_index_in_block == 0;
Utxo {
output,
height,
from_coinbase,
}
}
}
impl OrderedUtxo {
/// Create a new ordered UTXO from its fields.
pub fn new(
output: transparent::Output,
height: block::Height,
tx_index_in_block: usize,
) -> OrderedUtxo {
// Coinbase transactions are always the first transaction in their block,
// we check the other consensus rules separately.
let from_coinbase = tx_index_in_block == 0;
OrderedUtxo {
utxo: Utxo::new(output, height, from_coinbase),
tx_index_in_block,
}
}
/// Create a new ordered UTXO from a UTXO and transaction index.
pub fn from_utxo(utxo: Utxo, tx_index_in_block: usize) -> OrderedUtxo {
OrderedUtxo {
utxo,
tx_index_in_block,
}
}
}
/// A restriction that must be checked before spending a transparent output of a
/// coinbase transaction.
///
/// See the function `transparent_coinbase_spend` in `zebra-state` for the
/// consensus rules.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "proptest-impl"),
derive(proptest_derive::Arbitrary)
)]
pub enum CoinbaseSpendRestriction {
/// The UTXO is spent in a transaction with one or more transparent outputs
SomeTransparentOutputs,
/// The UTXO is spent in a transaction which only has shielded outputs
OnlyShieldedOutputs {
/// The height at which the UTXO is spent
spend_height: block::Height,
},
}
/// Compute an index of [`Utxo`]s, given an index of [`OrderedUtxo`]s.
pub fn utxos_from_ordered_utxos(
ordered_utxos: HashMap<transparent::OutPoint, OrderedUtxo>,
) -> HashMap<transparent::OutPoint, Utxo> {
ordered_utxos
.into_iter()
.map(|(outpoint, ordered_utxo)| (outpoint, ordered_utxo.utxo))
.collect()
}
/// Compute an index of [`transparent::Output`]s, given an index of [`Utxo`]s.
pub fn outputs_from_utxos(
utxos: HashMap<transparent::OutPoint, Utxo>,
) -> HashMap<transparent::OutPoint, transparent::Output> {
utxos
.into_iter()
.map(|(outpoint, utxo)| (outpoint, utxo.output))
.collect()
}
/// Compute an index of newly created [`Utxo`]s, given a block and a
/// list of precomputed transaction hashes.
pub fn new_outputs(
block: &Block,
transaction_hashes: &[transaction::Hash],
) -> HashMap<transparent::OutPoint, Utxo> {
utxos_from_ordered_utxos(new_ordered_outputs(block, transaction_hashes))
}
/// Compute an index of newly created [`Utxo`]s, given a block and a
/// list of precomputed transaction hashes.
///
/// This is a test-only function, prefer [`new_outputs`].
#[cfg(any(test, feature = "proptest-impl"))]
pub fn new_outputs_with_height(
block: &Block,
height: Height,
transaction_hashes: &[transaction::Hash],
) -> HashMap<transparent::OutPoint, Utxo> {
utxos_from_ordered_utxos(new_ordered_outputs_with_height(
block,
height,
transaction_hashes,
))
}
/// Compute an index of newly created [`OrderedUtxo`]s, given a block and a
/// list of precomputed transaction hashes.
pub fn new_ordered_outputs(
block: &Block,
transaction_hashes: &[transaction::Hash],
) -> HashMap<transparent::OutPoint, OrderedUtxo> {
let height = block.coinbase_height().expect("block has coinbase height");
new_ordered_outputs_with_height(block, height, transaction_hashes)
}
/// Compute an index of newly created [`OrderedUtxo`]s, given a block and a
/// list of precomputed transaction hashes.
///
/// This function is intended for use in this module, and in tests.
/// Prefer [`new_ordered_outputs`].
pub fn new_ordered_outputs_with_height(
block: &Block,
height: Height,
transaction_hashes: &[transaction::Hash],
) -> HashMap<transparent::OutPoint, OrderedUtxo> {
let mut new_ordered_outputs = HashMap::new();
for (tx_index_in_block, (transaction, hash)) in block
.transactions
.iter()
.zip(transaction_hashes.iter().cloned())
.enumerate()
{
new_ordered_outputs.extend(new_transaction_ordered_outputs(
transaction,
hash,
tx_index_in_block,
height,
));
}
new_ordered_outputs
}
/// Compute an index of newly created [`OrderedUtxo`]s, given a transaction,
/// its precomputed transaction hash, the transaction's index in its block,
/// and the block's height.
///
/// This function is only for use in this module, and in tests.
pub fn new_transaction_ordered_outputs(
transaction: &Transaction,
hash: transaction::Hash,
tx_index_in_block: usize,
height: block::Height,
) -> HashMap<transparent::OutPoint, OrderedUtxo> {
let mut new_ordered_outputs = HashMap::new();
for (output_index_in_transaction, output) in transaction.outputs().iter().cloned().enumerate() {
let output_index_in_transaction = output_index_in_transaction
.try_into()
.expect("unexpectedly large number of outputs");
new_ordered_outputs.insert(
transparent::OutPoint {
hash,
index: output_index_in_transaction,
},
OrderedUtxo::new(output, height, tx_index_in_block),
);
}
new_ordered_outputs
}