WIP: Removing transparent transactions
This commit is contained in:
parent
2ba837470e
commit
f748b29eca
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
mod prop;
|
||||
mod vectors;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue