Add Orchard bundle parsing & txid calculation
This commit is contained in:
parent
1d9dd128b9
commit
36dad6282e
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue