Add Orchard bundle parsing & txid calculation

This commit is contained in:
Kris Nuttycombe 2022-05-20 12:22:23 -06:00
parent 1d9dd128b9
commit 36dad6282e
6 changed files with 1641 additions and 30 deletions

View File

@ -1,6 +1,7 @@
//! Structs representing the components within Zcash transactions.
pub mod amount;
pub(crate) mod orchard;
pub mod sapling;
pub mod sprout;
pub mod transparent;

View File

@ -0,0 +1,262 @@
/// Functions for parsing & serialization of Orchard transaction components.
use std::io::{self, Read};
use byteorder::ReadBytesExt;
use super::Amount;
use crate::{serialize::Vector, transaction::Transaction};
use blake2b_simd::{Hash as Blake2bHash, Params, State};
const FLAG_SPENDS_ENABLED: u8 = 0b0000_0001;
const FLAG_OUTPUTS_ENABLED: u8 = 0b0000_0010;
const FLAGS_EXPECTED_UNSET: u8 = !(FLAG_SPENDS_ENABLED | FLAG_OUTPUTS_ENABLED);
const ZCASH_ORCHARD_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrchardHash";
const ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActCHash";
const ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActMHash";
const ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActNHash";
/// Orchard-specific flags.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Flags {
/// Flag denoting whether Orchard spends are enabled in the transaction.
///
/// If `false`, spent notes within [`Action`]s in the transaction's [`Bundle`] are
/// guaranteed to be dummy notes. If `true`, the spent notes may be either real or
/// dummy notes.
spends_enabled: bool,
/// Flag denoting whether Orchard outputs are enabled in the transaction.
///
/// If `false`, created notes within [`Action`]s in the transaction's [`Bundle`] are
/// guaranteed to be dummy notes. If `true`, the created notes may be either real or
/// dummy notes.
outputs_enabled: bool,
}
impl Flags {
/// Construct a set of flags from its constituent parts
fn from_parts(spends_enabled: bool, outputs_enabled: bool) -> Self {
Flags {
spends_enabled,
outputs_enabled,
}
}
fn to_byte(&self) -> u8 {
let mut value = 0u8;
if self.spends_enabled {
value |= FLAG_SPENDS_ENABLED;
}
if self.outputs_enabled {
value |= FLAG_OUTPUTS_ENABLED;
}
value
}
fn from_byte(value: u8) -> Option<Self> {
if value & FLAGS_EXPECTED_UNSET == 0 {
Some(Self::from_parts(
value & FLAG_SPENDS_ENABLED != 0,
value & FLAG_OUTPUTS_ENABLED != 0,
))
} else {
None
}
}
}
/// An encrypted note.
#[derive(Clone)]
pub(crate) struct TransmittedNoteCiphertext {
pub epk_bytes: [u8; 32],
pub enc_ciphertext: [u8; 580],
pub out_ciphertext: [u8; 80],
}
pub(crate) struct Action<A> {
nf: [u8; 32],
rk: [u8; 32],
cmx: [u8; 32],
encrypted_note: TransmittedNoteCiphertext,
cv_net: [u8; 32],
authorization: A,
}
impl<T> Action<T> {
/// Transitions this action from one authorization state to another.
pub fn try_map<U, E>(self, step: impl FnOnce(T) -> Result<U, E>) -> Result<Action<U>, E> {
Ok(Action {
nf: self.nf,
rk: self.rk,
cmx: self.cmx,
encrypted_note: self.encrypted_note,
cv_net: self.cv_net,
authorization: step(self.authorization)?,
})
}
}
pub(crate) struct Bundle {
actions: Vec<Action<[u8; 64]>>,
flags: Flags,
value_balance: Amount,
anchor: [u8; 32],
}
/// Reads an [`orchard::Bundle`] from a v5 transaction format.
pub(crate) fn read_v5_bundle<R: Read>(mut reader: R) -> io::Result<Option<Bundle>> {
#[allow(clippy::redundant_closure)]
let actions_without_auth = Vector::read(&mut reader, |r| read_action_without_auth(r))?;
if actions_without_auth.is_empty() {
Ok(None)
} else {
let flags = read_flags(&mut reader)?;
let value_balance = Transaction::read_amount(&mut reader)?;
let anchor = read_anchor(&mut reader)?;
let _proof_bytes = Vector::read(&mut reader, |r| r.read_u8())?;
let actions = actions_without_auth
.into_iter()
.map(|act| act.try_map(|_| read_signature(&mut reader)))
.collect::<Result<Vec<_>, _>>()?;
let _binding_signature = read_signature(&mut reader)?;
Ok(Some(Bundle {
actions,
flags,
value_balance,
anchor,
}))
}
}
fn read_value_commitment<R: Read>(mut reader: R) -> io::Result<[u8; 32]> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn read_nullifier<R: Read>(mut reader: R) -> io::Result<[u8; 32]> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn read_verification_key<R: Read>(mut reader: R) -> io::Result<[u8; 32]> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn read_cmx<R: Read>(mut reader: R) -> io::Result<[u8; 32]> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn read_note_ciphertext<R: Read>(mut reader: R) -> io::Result<TransmittedNoteCiphertext> {
let mut tnc = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
out_ciphertext: [0u8; 80],
};
reader.read_exact(&mut tnc.epk_bytes)?;
reader.read_exact(&mut tnc.enc_ciphertext)?;
reader.read_exact(&mut tnc.out_ciphertext)?;
Ok(tnc)
}
fn read_action_without_auth<R: Read>(mut reader: R) -> io::Result<Action<()>> {
let cv_net = read_value_commitment(&mut reader)?;
let nf_old = read_nullifier(&mut reader)?;
let rk = read_verification_key(&mut reader)?;
let cmx = read_cmx(&mut reader)?;
let encrypted_note = read_note_ciphertext(&mut reader)?;
Ok(Action {
nf: nf_old,
rk,
cmx,
encrypted_note,
cv_net,
authorization: (),
})
}
fn read_flags<R: Read>(mut reader: R) -> io::Result<Flags> {
let mut byte = [0u8; 1];
reader.read_exact(&mut byte)?;
Flags::from_byte(byte[0]).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Orchard flags".to_owned(),
)
})
}
fn read_anchor<R: Read>(mut reader: R) -> io::Result<[u8; 32]> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn read_signature<R: Read>(mut reader: R) -> io::Result<[u8; 64]> {
let mut bytes = [0u8; 64];
reader.read_exact(&mut bytes)?;
Ok(bytes)
}
fn hasher(personal: &[u8; 16]) -> State {
Params::new().hash_length(32).personal(personal).to_state()
}
/// Write disjoint parts of each Orchard shielded action as 3 separate hashes:
/// * \[(nullifier, cmx, ephemeral_key, enc_ciphertext\[..52\])*\] personalized
/// with ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION
/// * \[enc_ciphertext\[52..564\]*\] (memo ciphertexts) personalized
/// with ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION
/// * \[(cv, rk, enc_ciphertext\[564..\], out_ciphertext)*\] personalized
/// with ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION
/// as defined in [ZIP-244: Transaction Identifier Non-Malleability][zip244]
///
/// Then, hash these together along with (flags, value_balance_orchard, anchor_orchard),
/// personalized with ZCASH_ORCHARD_ACTIONS_HASH_PERSONALIZATION
///
/// [zip244]: https://zips.z.cash/zip-0244
pub(crate) fn hash_bundle_txid_data(bundle: &Bundle) -> Blake2bHash {
let mut h = hasher(ZCASH_ORCHARD_HASH_PERSONALIZATION);
let mut ch = hasher(ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION);
let mut mh = hasher(ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION);
let mut nh = hasher(ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION);
for action in bundle.actions.iter() {
ch.update(&action.nf);
ch.update(&action.cmx);
ch.update(&action.encrypted_note.epk_bytes);
ch.update(&action.encrypted_note.enc_ciphertext[..52]);
mh.update(&action.encrypted_note.enc_ciphertext[52..564]);
nh.update(&action.cv_net);
nh.update(&action.rk);
nh.update(&action.encrypted_note.enc_ciphertext[564..]);
nh.update(&action.encrypted_note.out_ciphertext);
}
h.update(ch.finalize().as_bytes());
h.update(mh.finalize().as_bytes());
h.update(nh.finalize().as_bytes());
h.update(&[bundle.flags.to_byte()]);
h.update(&bundle.value_balance.to_i64_le_bytes());
h.update(&bundle.anchor);
h.finalize()
}
/// Construct the commitment for the absent bundle as defined in
/// [ZIP-244: Transaction Identifier Non-Malleability][zip244]
///
/// [zip244]: https://zips.z.cash/zip-0244
pub fn hash_bundle_txid_empty() -> Blake2bHash {
hasher(ZCASH_ORCHARD_HASH_PERSONALIZATION).finalize()
}

View File

@ -9,10 +9,11 @@ use std::ops::Deref;
use crate::{
consensus::{BlockHeight, BranchId},
sapling::redjubjub::Signature,
serialize::{Array, CompactSize, Vector},
serialize::{Array, Vector},
};
use self::{
components::orchard,
txid::to_txid,
util::sha256d::{HashReader, HashWriter},
};
@ -185,19 +186,6 @@ impl TxVersion {
pub fn has_tze(&self) -> bool {
matches!(self, TxVersion::ZFuture)
}
pub fn suggested_for_branch(consensus_branch_id: BranchId) -> Self {
match consensus_branch_id {
BranchId::Sprout => TxVersion::Sprout(2),
BranchId::Overwinter => TxVersion::Overwinter,
BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => {
TxVersion::Sapling
}
BranchId::Nu5 => TxVersion::Zip225,
#[cfg(feature = "zfuture")]
BranchId::ZFuture => TxVersion::ZFuture,
}
}
}
/// A Zcash transaction.
@ -377,7 +365,7 @@ impl Transaction {
let version = TxVersion::read(&mut reader)?;
match version {
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
Self::read_v4(&mut reader, version)
Self::read_v4(reader, version)
}
TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version),
#[cfg(feature = "zfuture")]
@ -386,9 +374,7 @@ impl Transaction {
}
#[allow(clippy::redundant_closure)]
fn read_v4<R: Read>(reader: R, version: TxVersion) -> io::Result<Self> {
let mut reader = HashReader::new(reader);
fn read_v4<R: Read>(mut reader: HashReader<R>, version: TxVersion) -> io::Result<Self> {
let is_overwinter_v3 = version == TxVersion::Overwinter;
let is_sapling_v4 = version == TxVersion::Sapling;
@ -497,9 +483,7 @@ impl Transaction {
let (value_balance, shielded_spends, shielded_outputs, binding_sig) =
Self::read_v5_sapling(&mut reader)?;
// we do not attempt to parse the Orchard bundle, but we validate its
// presence
let _ = CompactSize::read(&mut reader)?;
let orchard_bundle = orchard::read_v5_bundle(&mut reader)?;
#[cfg(feature = "zfuture")]
let (tze_inputs, tze_outputs) = if version.has_tze() {
@ -529,7 +513,7 @@ impl Transaction {
binding_sig,
};
let txid = to_txid(&data, consensus_branch_id);
let txid = to_txid(&data, orchard_bundle.as_ref(), consensus_branch_id);
Ok(Transaction { txid, data })
}

View File

@ -27,6 +27,14 @@ fn tx_read_write() {
assert_eq!(&data[..], &encoded[..]);
}
#[test]
fn read_v5_txid() {
for v5_tv in &self::data::tx_read_write::v5_test_vectors() {
let tx = Transaction::read(&v5_tv.tx[..]).unwrap();
assert_eq!(v5_tv.txid, tx.txid().0);
}
}
#[test]
fn tx_write_rejects_unexpected_joinsplit_pubkey() {
// Succeeds without a JoinSplit pubkey

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ use crate::consensus::{BlockHeight, BranchId};
use super::{
components::{
orchard,
sapling::{OutputDescription, SpendDescription},
transparent::{TxIn, TxOut},
},
@ -27,7 +28,6 @@ const ZCASH_TX_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashTxHash_";
const ZCASH_HEADERS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdHeadersHash";
pub(crate) const ZCASH_TRANSPARENT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTranspaHash";
const ZCASH_SAPLING_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSaplingHash";
const ZCASH_ORCHARD_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrchardHash";
#[cfg(feature = "zfuture")]
const ZCASH_TZE_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTZE____Hash";
@ -246,6 +246,7 @@ pub(crate) fn to_hash(
header_digest: Blake2bHash,
transparent_digest: Blake2bHash,
sapling_digest: Blake2bHash,
orchard_digest: Blake2bHash,
#[cfg(feature = "zfuture")] tze_digest: Option<Blake2bHash>,
) -> Blake2bHash {
let mut personal = [0; 16];
@ -258,12 +259,7 @@ pub(crate) fn to_hash(
h.write_all(header_digest.as_bytes()).unwrap();
h.write_all(transparent_digest.as_bytes()).unwrap();
h.write_all(sapling_digest.as_bytes()).unwrap();
h.write_all(
hasher(ZCASH_ORCHARD_HASH_PERSONALIZATION)
.finalize()
.as_bytes(),
)
.unwrap();
h.write_all(orchard_digest.as_bytes()).unwrap();
#[cfg(feature = "zfuture")]
if let Some(digest) = tze_digest {
@ -273,7 +269,11 @@ pub(crate) fn to_hash(
h.finalize()
}
pub fn to_txid(txdata: &TransactionData, consensus_branch_id: BranchId) -> TxId {
pub(crate) fn to_txid(
txdata: &TransactionData,
orchard_bundle: Option<&orchard::Bundle>,
consensus_branch_id: BranchId,
) -> TxId {
let txid_digest = to_hash(
consensus_branch_id,
hash_header_txid_data(
@ -284,6 +284,10 @@ pub fn to_txid(txdata: &TransactionData, consensus_branch_id: BranchId) -> TxId
),
hash_transparent_txid_data(&txdata),
hash_sapling_txid_data(&txdata),
orchard_bundle.map_or_else(
|| orchard::hash_bundle_txid_empty(),
orchard::hash_bundle_txid_data,
),
#[cfg(feature = "zfuture")]
if txdata.version.has_tze() {
Some(hash_tze_txid_data(&txdata))