#![cfg_attr(feature = "std", doc = "## Feature flags")]
#![cfg_attr(feature = "std", doc = document_features::document_features!())]
#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, doc(auto_cfg))]
#![deny(rustdoc::broken_intra_doc_links)]
#[macro_use]
extern crate alloc;
use alloc::vec::Vec;
use getset::Getters;
#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
use zcash_protocol::constants::{V6_TX_VERSION, V6_VERSION_GROUP_ID};
#[cfg(all(
any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"),
zcash_unstable = "nu7",
feature = "zip-233",
))]
use zcash_protocol::value::Zatoshis;
#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
use {
common::{Global, determine_lock_time},
zcash_primitives::transaction::{Authorization, TransactionData, TxVersion},
zcash_protocol::{
consensus::{BranchId, OrchardProtocolRevision},
constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID},
},
};
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
use zcash_primitives::transaction::sighash_v6::v6_signature_hash;
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
use {
blake2b_simd::Hash as Blake2bHash,
zcash_primitives::transaction::{
TxDigests, sighash::SignableInput, sighash_v5::v5_signature_hash,
},
};
pub mod roles;
pub mod common;
pub mod orchard;
pub mod sapling;
pub mod transparent;
pub(crate) const MAGIC_BYTES: &[u8; 4] = b"PCZT";
pub(crate) const PCZT_VERSION_1: u32 = 1;
pub(crate) const PCZT_VERSION_2: u32 = 2;
const VERSIONED_HEADER_LEN: usize = 8;
pub(crate) enum HeaderParseError {
InvalidMagic,
TooShort,
}
pub(crate) fn parse_header<'a>(
bytes: &'a [u8],
magic: &[u8; 4],
) -> Result<(u32, &'a [u8]), HeaderParseError> {
if bytes.len() < VERSIONED_HEADER_LEN {
return Err(HeaderParseError::TooShort);
}
if &bytes[..4] != magic {
return Err(HeaderParseError::InvalidMagic);
}
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
Ok((version, &bytes[VERSIONED_HEADER_LEN..]))
}
pub(crate) fn serialize_header(magic: &[u8; 4], version: u32) -> Vec<u8> {
let mut bytes = Vec::with_capacity(VERSIONED_HEADER_LEN);
bytes.extend_from_slice(magic);
bytes.extend_from_slice(&version.to_le_bytes());
bytes
}
pub fn parse(bytes: &[u8]) -> Result<Pczt, ParseError> {
Pczt::parse(bytes)
}
#[derive(Clone, Debug, Getters)]
pub struct Pczt {
#[getset(get = "pub")]
pub(crate) global: common::Global,
#[getset(get = "pub")]
pub(crate) transparent: transparent::Bundle,
#[getset(get = "pub")]
pub(crate) sapling: sapling::Bundle,
#[getset(get = "pub")]
pub(crate) orchard: orchard::Bundle,
#[getset(get = "pub")]
pub(crate) ironwood: orchard::Bundle,
}
pub mod v1 {
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::{common, orchard, sapling, transparent};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Pczt {
global: common::Global,
transparent: transparent::Bundle,
sapling: sapling::v1::Bundle,
orchard: orchard::v1::Bundle,
}
impl Pczt {
pub fn serialize(&self) -> Vec<u8> {
let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_1);
postcard::to_extend(&self, bytes).expect("can serialize into memory")
}
}
impl TryFrom<super::Pczt> for Pczt {
type Error = super::EncodingError;
fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
if pczt.global.tx_version == zcash_protocol::constants::V6_TX_VERSION {
return Err(super::EncodingError::UnsupportedTxVersion);
}
if pczt.ironwood != orchard::EMPTY_IRONWOOD {
return Err(super::EncodingError::UnsupportedTxVersion);
}
Ok(Self {
global: pczt.global,
transparent: pczt.transparent,
sapling: sapling::v1::Bundle::try_from(pczt.sapling)?,
orchard: orchard::v1::Bundle::try_from(pczt.orchard)?,
})
}
}
impl From<Pczt> for super::Pczt {
fn from(pczt: Pczt) -> Self {
Self {
global: pczt.global,
transparent: pczt.transparent,
sapling: pczt.sapling.into(),
orchard: pczt.orchard.into(),
ironwood: orchard::EMPTY_IRONWOOD,
}
}
}
#[cfg(test)]
mod tests {
use zcash_protocol::consensus::BranchId;
use crate::roles::creator::Creator;
#[test]
fn v1_refuses_v6_pczts_and_non_canonical_ironwood_bundles() {
let pczt = Creator::new(
BranchId::Nu6_3.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
assert!(matches!(
super::Pczt::try_from(pczt),
Err(crate::EncodingError::UnsupportedTxVersion)
));
let mut pczt = Creator::new(
BranchId::Nu6.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
pczt.ironwood.bsk = Some([1; 32]);
assert!(matches!(
super::Pczt::try_from(pczt),
Err(crate::EncodingError::UnsupportedTxVersion)
));
}
}
}
pub mod v2 {
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::{common, orchard, sapling, transparent};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Pczt {
global: common::Global,
transparent: Option<transparent::Bundle>,
sapling: Option<sapling::Bundle>,
orchard: Option<orchard::v2::Bundle>,
ironwood: Option<orchard::v2::Bundle>,
}
impl Pczt {
pub fn serialize(&self) -> Vec<u8> {
let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_2);
postcard::to_extend(&self, bytes).expect("can serialize into memory")
}
}
impl TryFrom<super::Pczt> for Pczt {
type Error = super::EncodingError;
fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
Ok(Self {
global: pczt.global,
transparent: (pczt.transparent != transparent::EMPTY_BUNDLE)
.then_some(pczt.transparent),
sapling: sapling::v2::encode(pczt.sapling),
orchard: orchard::v2::encode(pczt.orchard, &orchard::EMPTY_ORCHARD)?,
ironwood: orchard::v2::encode(pczt.ironwood, &orchard::EMPTY_IRONWOOD)?,
})
}
}
impl Pczt {
pub(super) fn into_logical(self) -> Result<super::Pczt, super::ParseError> {
Ok(super::Pczt {
global: self.global,
transparent: self.transparent.unwrap_or(transparent::EMPTY_BUNDLE),
sapling: self.sapling.unwrap_or(sapling::EMPTY_BUNDLE),
orchard: self
.orchard
.map(orchard::v2::Bundle::into_logical)
.transpose()?
.unwrap_or(orchard::EMPTY_ORCHARD),
ironwood: self
.ironwood
.map(orchard::v2::Bundle::into_logical)
.transpose()?
.unwrap_or(orchard::EMPTY_IRONWOOD),
})
}
}
#[cfg(test)]
mod tests {
use zcash_protocol::consensus::BranchId;
use super::Pczt;
use crate::{orchard::NoteVersion, roles::creator::Creator};
#[test]
fn empty_bundles_encode_as_none_and_decode_as_empty() {
let pczt = Creator::new(BranchId::Nu6_3.into(), 10_000_000, 133, None, None)
.unwrap()
.build()
.unwrap();
let encoded = Pczt::try_from(pczt).unwrap();
assert!(encoded.transparent.is_none());
assert!(encoded.sapling.is_none());
assert!(encoded.orchard.is_none());
assert!(encoded.ironwood.is_none());
let decoded = crate::parse(&encoded.serialize()).unwrap();
assert!(decoded.transparent.inputs.is_empty());
assert!(decoded.transparent.outputs.is_empty());
assert!(decoded.sapling.spends.is_empty());
assert!(decoded.sapling.outputs.is_empty());
assert!(decoded.sapling.anchor.is_none());
assert!(decoded.orchard.actions.is_empty());
assert_eq!(decoded.orchard.note_version, NoteVersion::V2);
{
assert!(decoded.ironwood.actions.is_empty());
assert_eq!(decoded.ironwood.note_version, NoteVersion::V3);
}
}
#[test]
fn anchored_bundles_are_preserved() {
let pczt = Creator::new(
BranchId::Nu6.into(),
10_000_000,
133,
Some([1; 32]),
Some([2; 32]),
)
.unwrap()
.build()
.unwrap();
let encoded = Pczt::try_from(pczt).unwrap();
assert!(encoded.transparent.is_none());
assert!(encoded.sapling.is_some());
assert!(encoded.orchard.is_some());
let decoded = crate::parse(&encoded.serialize()).unwrap();
assert_eq!(decoded.sapling.anchor, Some([1; 32]));
assert_eq!(decoded.orchard.anchor, Some([2; 32]));
}
#[test]
fn non_canonical_orchard_flags_and_note_version_prevent_omission() {
let mut pczt = Creator::new(
BranchId::Nu6.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
pczt.orchard.flags = 0;
pczt.orchard.note_version = NoteVersion::V3;
let encoded = Pczt::try_from(pczt.clone()).unwrap();
assert!(encoded.orchard.is_some());
let decoded = encoded.into_logical().unwrap();
assert_eq!(decoded.orchard, pczt.orchard);
assert_eq!(decoded.orchard.flags, 0);
assert_eq!(decoded.orchard.note_version, NoteVersion::V3);
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum EncodingError {
UnsupportedTxVersion,
UnsupportedOrchardNoteVersion,
RequiresV2,
}
impl Pczt {
pub fn parse(bytes: &[u8]) -> Result<Self, ParseError> {
let (version, body) = parse_header(bytes, MAGIC_BYTES).map_err(|e| match e {
HeaderParseError::InvalidMagic => ParseError::NotPczt,
HeaderParseError::TooShort => ParseError::TooShort,
})?;
match version {
PCZT_VERSION_1 => postcard::from_bytes::<v1::Pczt>(body)
.map(Pczt::from)
.map_err(ParseError::Invalid),
PCZT_VERSION_2 => postcard::from_bytes::<v2::Pczt>(body)
.map_err(ParseError::Invalid)
.and_then(v2::Pczt::into_logical),
_ => Err(ParseError::UnknownVersion(version)),
}
}
pub fn serialize(self) -> Result<Vec<u8>, EncodingError> {
Ok(v2::Pczt::try_from(self)?.serialize())
}
#[cfg(feature = "orchard")]
pub fn resolve_fields(&mut self) -> Result<(), ::orchard::pczt::ParseError> {
self.orchard.resolve_fields()?;
self.ironwood.resolve_fields()
}
#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
pub(crate) fn extract_tx_data<A, E>(
self,
anchor_requirement: common::AnchorRequirement,
extract_transparent: impl FnOnce(
&::transparent::pczt::Bundle,
) -> Result<
Option<::transparent::bundle::Bundle<A::TransparentAuth>>,
E,
>,
extract_sapling: impl FnOnce(
&::sapling::pczt::Bundle,
) -> Result<
Option<::sapling::Bundle<A::SaplingAuth, zcash_protocol::value::ZatBalance>>,
E,
>,
extract_orchard: impl FnOnce(
&::orchard::pczt::Bundle,
) -> Result<
Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
E,
>,
extract_ironwood: impl FnOnce(
&::orchard::pczt::Bundle,
) -> Result<
Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
E,
>,
) -> Result<ParsedPczt<A>, E>
where
A: Authorization,
E: From<ExtractError>,
{
let Pczt {
global,
transparent,
sapling,
orchard,
ironwood,
} = self;
let consensus_branch_id = BranchId::try_from(global.consensus_branch_id)
.map_err(|_| ExtractError::UnknownConsensusBranchId)?;
let orchard_protocol_revision = consensus_branch_id
.orchard_protocol_revision()
.ok_or(ExtractError::UnsupportedConsensusBranchId)?;
let version = match (global.tx_version, global.version_group_id) {
(V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::V5),
(V6_TX_VERSION, V6_VERSION_GROUP_ID) => Ok(TxVersion::V6),
(version, version_group_id) => Err(ExtractError::UnsupportedTxVersion {
version,
version_group_id,
}),
}?;
match version {
TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 | TxVersion::V5 => {
if ironwood != crate::orchard::EMPTY_IRONWOOD {
return Err(ExtractError::IronwoodNotSupported.into());
}
}
TxVersion::V6 => {
if orchard_protocol_revision < OrchardProtocolRevision::V3 {
return Err(ExtractError::UnsupportedConsensusBranchId.into());
}
}
}
let transparent = transparent
.into_parsed()
.map_err(ExtractError::TransparentParse)?;
let sapling = sapling
.into_parsed(anchor_requirement)
.map_err(ExtractError::SaplingParse)?;
let orchard_bundle_version = crate::orchard::bundle_version_for_revision(
orchard_protocol_revision,
::orchard::ValuePool::Orchard,
)
.expect("the Orchard pool is supported under every protocol revision");
let orchard = orchard
.into_parsed_with_version(orchard_bundle_version, anchor_requirement)
.map_err(ExtractError::OrchardParse)?;
let ironwood = ironwood
.into_ironwood_parsed(anchor_requirement)
.map_err(ExtractError::IronwoodParse)?;
let lock_time = determine_lock_time(&global, transparent.inputs())
.ok_or(ExtractError::IncompatibleLockTimes)?;
let transparent_bundle = extract_transparent(&transparent)?;
let sapling_bundle = extract_sapling(&sapling.bundle)?;
let orchard_bundle = extract_orchard(&orchard.bundle)?;
let ironwood_bundle = extract_ironwood(&ironwood.bundle)?;
let tx_data = match version {
TxVersion::V6 => TransactionData::from_parts_v6(
consensus_branch_id,
lock_time,
global.expiry_height.into(),
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
Zatoshis::ZERO,
transparent_bundle,
sapling_bundle,
orchard_bundle,
ironwood_bundle,
),
_ => TransactionData::from_parts(
version,
consensus_branch_id,
lock_time,
global.expiry_height.into(),
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
Zatoshis::ZERO,
transparent_bundle,
None,
sapling_bundle,
orchard_bundle,
),
};
Ok(ParsedPczt {
global,
transparent,
sapling,
orchard,
ironwood,
tx_data,
})
}
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
pub fn into_effects(self) -> Result<TransactionData<EffectsOnly>, ExtractError> {
let anchor_requirement =
common::AnchorRequirement::for_pre_authorization(self.global.tx_version);
self.extract_tx_data(
anchor_requirement,
|t| {
t.extract_effects()
.map_err(ExtractError::TransparentExtract)
},
|s| s.extract_effects().map_err(ExtractError::SaplingExtract),
|o| o.extract_effects().map_err(ExtractError::OrchardExtract),
|i| i.extract_effects().map_err(ExtractError::IronwoodExtract),
)
.map(|parsed| parsed.tx_data)
}
}
#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
#[cfg_attr(
not(any(feature = "io-finalizer", feature = "signer")),
allow(dead_code)
)]
pub(crate) struct ParsedPczt<A: Authorization> {
pub(crate) global: Global,
pub(crate) transparent: ::transparent::pczt::Bundle,
pub(crate) sapling: crate::sapling::Parsed,
pub(crate) orchard: crate::orchard::Parsed,
pub(crate) ironwood: crate::orchard::Parsed,
pub(crate) tx_data: TransactionData<A>,
}
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
pub struct EffectsOnly;
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
impl Authorization for EffectsOnly {
type TransparentAuth = ::transparent::bundle::EffectsOnly;
type SaplingAuth = ::sapling::bundle::EffectsOnly;
type OrchardAuth = ::orchard::bundle::EffectsOnly;
}
#[cfg(any(feature = "io-finalizer", feature = "signer"))]
pub(crate) fn sighash(
tx_data: &TransactionData<EffectsOnly>,
signable_input: &SignableInput,
txid_parts: &TxDigests<Blake2bHash>,
) -> [u8; 32] {
match tx_data.version() {
TxVersion::V5 => v5_signature_hash(tx_data, signable_input, txid_parts),
TxVersion::V6 => v6_signature_hash(tx_data, signable_input, txid_parts),
_ => unreachable!("PCZT only supports v5 and v6 transaction data"),
}
.as_ref()
.try_into()
.expect("correct length")
}
#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
#[derive(Debug)]
#[non_exhaustive]
pub enum ExtractError {
IncompatibleLockTimes,
IronwoodExtract(::orchard::pczt::TxExtractorError),
IronwoodNotSupported,
IronwoodParse(crate::orchard::ParseError),
OrchardExtract(::orchard::pczt::TxExtractorError),
OrchardParse(crate::orchard::ParseError),
SaplingExtract(::sapling::pczt::TxExtractorError),
SaplingParse(crate::sapling::ParseError),
TransparentExtract(::transparent::pczt::TxExtractorError),
TransparentParse(::transparent::pczt::ParseError),
UnknownConsensusBranchId,
UnsupportedConsensusBranchId,
UnsupportedTxVersion { version: u32, version_group_id: u32 },
}
#[derive(Debug)]
pub enum ParseError {
NotPczt,
Invalid(postcard::Error),
MissingRequiredField(&'static str),
TooShort,
UnknownVersion(u32),
}
#[cfg(all(test, any(feature = "io-finalizer", feature = "signer")))]
mod extraction_tests {
use zcash_protocol::consensus::BranchId;
use crate::{ExtractError, roles::creator::Creator};
#[test]
fn v5_pczt_with_ironwood_data_does_not_extract() {
let mut pczt = Creator::new(
BranchId::Nu6.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
pczt.ironwood.bsk = Some([1; 32]);
assert!(matches!(
pczt.into_effects(),
Err(ExtractError::IronwoodNotSupported)
));
}
#[test]
fn v6_pczt_with_pre_nu6_3_branch_does_not_extract() {
let mut pczt = Creator::new(
BranchId::Nu6_3.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
pczt.global.consensus_branch_id = BranchId::Nu6_2.into();
assert!(matches!(
pczt.into_effects(),
Err(ExtractError::UnsupportedConsensusBranchId)
));
}
}