use alloc::vec::Vec;
pub mod commitments;
#[cfg(feature = "circuit")]
mod batch;
#[cfg(feature = "circuit")]
pub use batch::{BatchError, BatchValidator};
use core::fmt;
use blake2b_simd::Hash as Blake2bHash;
use nonempty::NonEmpty;
use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
#[cfg(feature = "std")]
use memuse::DynamicUsage;
use crate::{
action::Action,
address::Address,
bundle::commitments::{hash_bundle_auth_data, hash_bundle_txid_data},
keys::{IncomingViewingKey, OutgoingViewingKey, PreparedIncomingViewingKey},
note::{Note, NoteVersion},
note_encryption::BundleDomain,
primitives::redpallas::{self, Binding, SpendAuth},
tree::Anchor,
value::{ValueCommitTrapdoor, ValueCommitment, ValueSum},
Proof, ProtocolVersion, ValuePool,
};
#[cfg(feature = "circuit")]
use crate::circuit::{Instance, OrchardCircuitVersion, VerifyingKey};
#[cfg(feature = "circuit")]
impl<T> Action<T> {
pub fn to_instance(&self, flags: Flags, anchor: Anchor) -> Instance {
Instance::from_parts(
anchor,
self.cv_net().clone(),
*self.nullifier(),
self.rk().clone(),
*self.cmx(),
flags,
)
.expect("this Action's rk is non-identity by construction (Action::from_parts)")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct BundleVersion {
value_pool: ValuePool,
protocol_version: ProtocolVersion,
}
impl BundleVersion {
pub const fn orchard_insecure_v1() -> Self {
Self {
value_pool: ValuePool::Orchard,
protocol_version: ProtocolVersion::InsecureV1,
}
}
pub const fn orchard_v2() -> Self {
Self {
value_pool: ValuePool::Orchard,
protocol_version: ProtocolVersion::V2,
}
}
pub const fn orchard_v3() -> Self {
Self {
value_pool: ValuePool::Orchard,
protocol_version: ProtocolVersion::V3,
}
}
pub const fn ironwood_v3() -> Self {
Self {
value_pool: ValuePool::Ironwood,
protocol_version: ProtocolVersion::V3,
}
}
pub fn value_pool(&self) -> ValuePool {
self.value_pool
}
pub fn protocol_version(&self) -> ProtocolVersion {
self.protocol_version
}
#[cfg(feature = "circuit")]
pub fn circuit_version(&self) -> OrchardCircuitVersion {
match self.protocol_version {
ProtocolVersion::InsecureV1 => OrchardCircuitVersion::InsecurePreNu6_2,
ProtocolVersion::V2 => OrchardCircuitVersion::FixedPostNu6_2,
ProtocolVersion::V3 => OrchardCircuitVersion::PostNu6_3,
}
}
pub fn note_version(&self) -> NoteVersion {
match self.value_pool {
ValuePool::Orchard => NoteVersion::V2,
ValuePool::Ironwood => NoteVersion::V3,
}
}
pub(crate) fn permits_cross_address_transfers(&self) -> bool {
!matches!(
(self.protocol_version, self.value_pool),
(ProtocolVersion::V3, ValuePool::Orchard)
)
}
pub fn default_flags(&self) -> Flags {
Flags::from_parts(true, true, self.permits_cross_address_transfers())
}
pub(crate) fn enforces_canonical_proof_size(&self) -> bool {
!matches!(self.protocol_version, ProtocolVersion::InsecureV1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TxVersion {
V5,
V6,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Flags {
spends_enabled: bool,
outputs_enabled: bool,
cross_address_enabled: bool,
}
const FLAG_SPENDS_ENABLED: u8 = 0b0000_0001;
const FLAG_OUTPUTS_ENABLED: u8 = 0b0000_0010;
const FLAG_V6_CROSS_ADDRESS_ENABLED: u8 = 0b0000_0100;
const FLAGS_ALWAYS_EXPECTED_UNSET: u8 =
!(FLAG_SPENDS_ENABLED | FLAG_OUTPUTS_ENABLED | FLAG_V6_CROSS_ADDRESS_ENABLED);
impl Flags {
pub(crate) const fn from_parts(
spends_enabled: bool,
outputs_enabled: bool,
cross_address_enabled: bool,
) -> Self {
Flags {
spends_enabled,
outputs_enabled,
cross_address_enabled,
}
}
pub const ENABLED: Flags = Flags {
spends_enabled: true,
outputs_enabled: true,
cross_address_enabled: true,
};
pub const SPENDS_DISABLED: Flags = Flags {
spends_enabled: false,
outputs_enabled: true,
cross_address_enabled: true,
};
pub const OUTPUTS_DISABLED: Flags = Flags {
spends_enabled: true,
outputs_enabled: false,
cross_address_enabled: true,
};
pub const CROSS_ADDRESS_DISABLED: Flags = Flags {
spends_enabled: true,
outputs_enabled: true,
cross_address_enabled: false,
};
pub fn spends_enabled(&self) -> bool {
self.spends_enabled
}
pub fn outputs_enabled(&self) -> bool {
self.outputs_enabled
}
pub fn cross_address_enabled(&self) -> bool {
self.cross_address_enabled
}
pub fn to_byte(&self, bundle_version: BundleVersion) -> Option<u8> {
let mut value = 0u8;
if self.spends_enabled {
value |= FLAG_SPENDS_ENABLED;
}
if self.outputs_enabled {
value |= FLAG_OUTPUTS_ENABLED;
}
match (bundle_version.value_pool, bundle_version.protocol_version) {
(ValuePool::Orchard, ProtocolVersion::InsecureV1 | ProtocolVersion::V2) => {
if !self.cross_address_enabled {
return None;
}
}
(ValuePool::Orchard, ProtocolVersion::V3) => {
if self.cross_address_enabled {
return None;
}
}
(ValuePool::Ironwood, ProtocolVersion::V3) => {
if self.cross_address_enabled {
value |= FLAG_V6_CROSS_ADDRESS_ENABLED;
}
}
(ValuePool::Ironwood, _) => return None,
}
Some(value)
}
pub fn from_byte(value: u8, bundle_version: BundleVersion) -> Option<Self> {
if value & FLAGS_ALWAYS_EXPECTED_UNSET != 0 {
return None;
}
let bit2 = value & FLAG_V6_CROSS_ADDRESS_ENABLED != 0;
if bit2 && bundle_version.value_pool == ValuePool::Orchard {
return None;
}
let cross_address_enabled = match bundle_version.protocol_version {
ProtocolVersion::InsecureV1 | ProtocolVersion::V2 => true,
ProtocolVersion::V3 => bit2,
};
Some(Self {
spends_enabled: value & FLAG_SPENDS_ENABLED != 0,
outputs_enabled: value & FLAG_OUTPUTS_ENABLED != 0,
cross_address_enabled,
})
}
}
pub trait Authorization: fmt::Debug {
type SpendAuth: fmt::Debug;
}
#[derive(Clone)]
pub struct Bundle<T: Authorization, V> {
actions: NonEmpty<Action<T::SpendAuth>>,
flags: Flags,
value_balance: V,
anchor: Anchor,
authorization: T,
bundle_version: BundleVersion,
}
impl<T: Authorization, V: fmt::Debug> fmt::Debug for Bundle<T, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct Actions<'a, T>(&'a NonEmpty<Action<T>>);
impl<T: fmt::Debug> fmt::Debug for Actions<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self.0.iter()).finish()
}
}
f.debug_struct("Bundle")
.field("actions", &Actions(&self.actions))
.field("flags", &self.flags)
.field("value_balance", &self.value_balance)
.field("anchor", &self.anchor)
.field("authorization", &self.authorization)
.field("bundle_version", &self.bundle_version)
.finish()
}
}
pub(crate) fn validate_proof_size(proof: &Proof, num_actions: usize) -> Result<(), BundleError> {
let expected = Proof::expected_proof_size(num_actions);
let actual = proof.as_ref().len();
if actual == expected {
Ok(())
} else {
Err(BundleError::NonCanonicalProofSize { expected, actual })
}
}
fn validate_flags(flags: &Flags, bundle_version: BundleVersion) -> Result<(), BundleError> {
if flags.to_byte(bundle_version).is_some() {
Ok(())
} else {
Err(BundleError::UnrepresentableFlags)
}
}
impl<T: Authorization, V> Bundle<T, V> {
pub(crate) fn from_parts_unchecked(
actions: NonEmpty<Action<T::SpendAuth>>,
flags: Flags,
value_balance: V,
anchor: Anchor,
authorization: T,
bundle_version: BundleVersion,
) -> Self {
debug_assert!(flags.to_byte(bundle_version).is_some());
Bundle {
actions,
flags,
value_balance,
anchor,
authorization,
bundle_version,
}
}
pub fn actions(&self) -> &NonEmpty<Action<T::SpendAuth>> {
&self.actions
}
pub fn flags(&self) -> &Flags {
&self.flags
}
pub fn value_balance(&self) -> &V {
&self.value_balance
}
pub fn anchor(&self) -> &Anchor {
&self.anchor
}
pub fn authorization(&self) -> &T {
&self.authorization
}
pub fn bundle_version(&self) -> BundleVersion {
self.bundle_version
}
pub fn flag_byte(&self) -> u8 {
self.flags
.to_byte(self.bundle_version)
.expect("flags are validated against the bundle version at construction")
}
pub fn try_map_value_balance<V0, E, F: FnOnce(V) -> Result<V0, E>>(
self,
f: F,
) -> Result<Bundle<T, V0>, E> {
Ok(Bundle {
actions: self.actions,
flags: self.flags,
value_balance: f(self.value_balance)?,
anchor: self.anchor,
authorization: self.authorization,
bundle_version: self.bundle_version,
})
}
pub fn map_authorization<R, U: Authorization>(
self,
context: &mut R,
mut spend_auth: impl FnMut(&mut R, &T, T::SpendAuth) -> U::SpendAuth,
step: impl FnOnce(&mut R, T) -> U,
) -> Bundle<U, V> {
let authorization = self.authorization;
Bundle {
actions: self
.actions
.map(|a| a.map(|a_auth| spend_auth(context, &authorization, a_auth))),
flags: self.flags,
value_balance: self.value_balance,
anchor: self.anchor,
authorization: step(context, authorization),
bundle_version: self.bundle_version,
}
}
pub fn try_map_authorization<R, U: Authorization, E>(
self,
context: &mut R,
mut spend_auth: impl FnMut(&mut R, &T, T::SpendAuth) -> Result<U::SpendAuth, E>,
step: impl FnOnce(&mut R, T) -> Result<U, E>,
) -> Result<Bundle<U, V>, E> {
let authorization = self.authorization;
let new_actions = self
.actions
.into_iter()
.map(|a| a.try_map(|a_auth| spend_auth(context, &authorization, a_auth)))
.collect::<Result<Vec<_>, E>>()?;
Ok(Bundle {
actions: NonEmpty::from_vec(new_actions).unwrap(),
flags: self.flags,
value_balance: self.value_balance,
anchor: self.anchor,
authorization: step(context, authorization)?,
bundle_version: self.bundle_version,
})
}
#[cfg(feature = "circuit")]
pub(crate) fn to_instances(&self) -> Vec<Instance> {
self.actions
.iter()
.map(|a| a.to_instance(self.flags, self.anchor))
.collect()
}
pub fn decrypt_outputs_with_keys(
&self,
keys: &[IncomingViewingKey],
) -> Vec<(usize, IncomingViewingKey, Note, Address, [u8; 512])> {
let prepared_keys: Vec<_> = keys
.iter()
.map(|ivk| (ivk, PreparedIncomingViewingKey::new(ivk)))
.collect();
self.actions
.iter()
.enumerate()
.filter_map(|(idx, action)| {
let domain = BundleDomain::for_action(action, self.bundle_version.note_version());
prepared_keys.iter().find_map(|(ivk, prepared_ivk)| {
try_note_decryption(&domain, prepared_ivk, action)
.map(|(n, a, m)| (idx, (*ivk).clone(), n, a, m))
})
})
.collect()
}
pub fn decrypt_output_with_key(
&self,
action_idx: usize,
key: &IncomingViewingKey,
) -> Option<(Note, Address, [u8; 512])> {
let prepared_ivk = PreparedIncomingViewingKey::new(key);
self.actions.get(action_idx).and_then(move |action| {
let domain = BundleDomain::for_action(action, self.bundle_version.note_version());
try_note_decryption(&domain, &prepared_ivk, action)
})
}
pub fn recover_outputs_with_ovks(
&self,
keys: &[OutgoingViewingKey],
) -> Vec<(usize, OutgoingViewingKey, Note, Address, [u8; 512])> {
self.actions
.iter()
.enumerate()
.filter_map(|(idx, action)| {
let domain = BundleDomain::for_action(action, self.bundle_version.note_version());
keys.iter().find_map(move |key| {
try_output_recovery_with_ovk(
&domain,
key,
action,
action.cv_net(),
&action.encrypted_note().out_ciphertext,
)
.map(|(n, a, m)| (idx, key.clone(), n, a, m))
})
})
.collect()
}
pub fn recover_output_with_ovk(
&self,
action_idx: usize,
key: &OutgoingViewingKey,
) -> Option<(Note, Address, [u8; 512])> {
self.actions.get(action_idx).and_then(move |action| {
let domain = BundleDomain::for_action(action, self.bundle_version.note_version());
try_output_recovery_with_ovk(
&domain,
key,
action,
action.cv_net(),
&action.encrypted_note().out_ciphertext,
)
})
}
}
impl<T: Authorization, V: Copy + Into<i64>> Bundle<T, V> {
pub fn commitment(&self, tx_version: TxVersion) -> Result<BundleCommitment, CommitmentError> {
hash_bundle_txid_data(self, tx_version).map(BundleCommitment)
}
pub fn binding_validating_key(&self) -> redpallas::VerificationKey<Binding> {
(self
.actions
.iter()
.map(|a| a.cv_net())
.sum::<ValueCommitment>()
- ValueCommitment::derive(
ValueSum::from_raw(self.value_balance.into()),
ValueCommitTrapdoor::zero(),
))
.into_bvk()
}
}
#[derive(Clone, Debug)]
pub struct EffectsOnly;
impl Authorization for EffectsOnly {
type SpendAuth = ();
}
impl<V> Bundle<EffectsOnly, V> {
pub fn from_parts(
actions: NonEmpty<Action<<EffectsOnly as Authorization>::SpendAuth>>,
flags: Flags,
value_balance: V,
anchor: Anchor,
authorization: EffectsOnly,
bundle_version: BundleVersion,
) -> Result<Self, BundleError> {
validate_flags(&flags, bundle_version)?;
Ok(Bundle::from_parts_unchecked(
actions,
flags,
value_balance,
anchor,
authorization,
bundle_version,
))
}
}
#[derive(Debug, Clone)]
pub struct Authorized {
proof: Proof,
binding_signature: redpallas::Signature<Binding>,
}
impl Authorization for Authorized {
type SpendAuth = redpallas::Signature<SpendAuth>;
}
impl Authorized {
pub fn from_parts(proof: Proof, binding_signature: redpallas::Signature<Binding>) -> Self {
Authorized {
proof,
binding_signature,
}
}
pub fn proof(&self) -> &Proof {
&self.proof
}
pub fn binding_signature(&self) -> &redpallas::Signature<Binding> {
&self.binding_signature
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BundleError {
NonCanonicalProofSize {
expected: usize,
actual: usize,
},
UnrepresentableFlags,
}
impl fmt::Display for BundleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BundleError::NonCanonicalProofSize { expected, actual } => write!(
f,
"Orchard proof has non-canonical length {actual}; expected {expected} bytes",
),
BundleError::UnrepresentableFlags => write!(
f,
"bundle flags are not representable under the bundle's value pool and protocol version",
),
}
}
}
impl core::error::Error for BundleError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommitmentError {
InvalidTransactionVersion,
}
impl fmt::Display for CommitmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CommitmentError::InvalidTransactionVersion => write!(
f,
"Ironwood bundles can only be committed in a v6 transaction",
),
}
}
}
impl core::error::Error for CommitmentError {}
impl<V> Bundle<Authorized, V> {
pub fn try_from_parts(
actions: NonEmpty<Action<<Authorized as Authorization>::SpendAuth>>,
flags: Flags,
value_balance: V,
anchor: Anchor,
authorization: Authorized,
bundle_version: BundleVersion,
) -> Result<Self, BundleError> {
if bundle_version.enforces_canonical_proof_size() {
validate_proof_size(authorization.proof(), actions.len())?;
}
validate_flags(&flags, bundle_version)?;
Ok(Bundle::from_parts_unchecked(
actions,
flags,
value_balance,
anchor,
authorization,
bundle_version,
))
}
pub fn authorizing_commitment(
&self,
tx_version: TxVersion,
) -> Result<BundleAuthorizingCommitment, CommitmentError> {
hash_bundle_auth_data(self, tx_version).map(BundleAuthorizingCommitment)
}
#[cfg(feature = "circuit")]
pub fn verify_proof(&self, vk: &VerifyingKey) -> Result<(), halo2_proofs::plonk::Error> {
self.authorization()
.proof()
.verify(vk, &self.to_instances())
}
}
#[cfg(feature = "std")]
impl<V: DynamicUsage> DynamicUsage for Bundle<Authorized, V> {
fn dynamic_usage(&self) -> usize {
self.actions.tail.dynamic_usage()
+ self.value_balance.dynamic_usage()
+ self.authorization.proof.dynamic_usage()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
let bounds = (
self.actions.tail.dynamic_usage_bounds(),
self.value_balance.dynamic_usage_bounds(),
self.authorization.proof.dynamic_usage_bounds(),
);
(
bounds.0 .0 + bounds.1 .0 + bounds.2 .0,
bounds
.0
.1
.zip(bounds.1 .1)
.zip(bounds.2 .1)
.map(|((a, b), c)| a + b + c),
)
}
}
#[derive(Debug)]
pub struct BundleCommitment(pub Blake2bHash);
impl From<BundleCommitment> for [u8; 32] {
fn from(commitment: BundleCommitment) -> Self {
commitment.0.as_bytes().try_into().unwrap()
}
}
#[derive(Debug)]
pub struct BundleAuthorizingCommitment(pub Blake2bHash);
#[cfg(any(test, feature = "test-dependencies"))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use alloc::vec::Vec;
use group::ff::FromUniformBytes;
use nonempty::NonEmpty;
use pasta_curves::pallas;
use rand::{rngs::StdRng, SeedableRng};
use reddsa::orchard::SpendAuth;
use proptest::collection::vec;
use proptest::prelude::*;
use crate::{
bundle::BundleVersion,
primitives::redpallas::{self, testing::arb_binding_signing_key},
value::{testing::arb_note_value_bounded, NoteValue, ValueSum, MAX_NOTE_VALUE},
Anchor, NoteVersion, Proof,
};
use super::{Action, Authorized, Bundle, Flags};
pub use crate::action::testing::{arb_action, arb_unauthorized_action};
pub type Unauthorized = super::EffectsOnly;
pub fn arb_bundle_version() -> impl Strategy<Value = BundleVersion> {
prop_oneof![
Just(BundleVersion::orchard_insecure_v1()),
Just(BundleVersion::orchard_v2()),
Just(BundleVersion::orchard_v3()),
Just(BundleVersion::ironwood_v3()),
]
}
fn flags_for_version(bundle_version: BundleVersion, flags: Flags) -> Flags {
Flags::from_parts(
flags.spends_enabled(),
flags.outputs_enabled(),
bundle_version.permits_cross_address_transfers(),
)
}
pub fn arb_unauthorized_action_n(
note_version: NoteVersion,
n_actions: usize,
flags: Flags,
) -> impl Strategy<Value = (ValueSum, Action<()>)> {
let spend_value_gen = if flags.spends_enabled {
Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64))
} else {
Strategy::boxed(Just(NoteValue::ZERO))
};
spend_value_gen.prop_flat_map(move |spend_value| {
let output_value_gen = if flags.outputs_enabled {
Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64))
} else {
Strategy::boxed(Just(NoteValue::ZERO))
};
output_value_gen.prop_flat_map(move |output_value| {
arb_unauthorized_action(note_version, spend_value, output_value)
.prop_map(move |a| (spend_value - output_value, a))
})
})
}
pub fn arb_action_n(
note_version: NoteVersion,
n_actions: usize,
flags: Flags,
) -> impl Strategy<Value = (ValueSum, Action<redpallas::Signature<SpendAuth>>)> {
let spend_value_gen = if flags.spends_enabled {
Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64))
} else {
Strategy::boxed(Just(NoteValue::ZERO))
};
spend_value_gen.prop_flat_map(move |spend_value| {
let output_value_gen = if flags.outputs_enabled {
Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64))
} else {
Strategy::boxed(Just(NoteValue::ZERO))
};
output_value_gen.prop_flat_map(move |output_value| {
arb_action(note_version, spend_value, output_value)
.prop_map(move |a| (spend_value - output_value, a))
})
})
}
prop_compose! {
pub fn arb_flags()(spends_enabled in prop::bool::ANY, outputs_enabled in prop::bool::ANY) -> Flags {
Flags::from_parts(spends_enabled, outputs_enabled, true)
}
}
prop_compose! {
pub fn arb_flags_ironwood_post_nu6_3()(
spends_enabled in prop::bool::ANY,
outputs_enabled in prop::bool::ANY,
cross_address_enabled in prop::bool::ANY,
) -> Flags {
Flags {
spends_enabled,
outputs_enabled,
cross_address_enabled,
}
}
}
prop_compose! {
fn arb_base()(bytes in prop::array::uniform32(0u8..)) -> pallas::Base {
let mut buf = [0; 64];
buf[..32].copy_from_slice(&bytes);
pallas::Base::from_uniform_bytes(&buf)
}
}
prop_compose! {
pub fn arb_unauthorized_bundle(n_actions: usize)
(
bundle_version in arb_bundle_version(),
flags in arb_flags(),
)
(
acts in vec(arb_unauthorized_action_n(bundle_version.note_version(), n_actions, flags), n_actions),
anchor in arb_base().prop_map(Anchor::from),
flags in Just(flags),
bundle_version in Just(bundle_version),
) -> Bundle<Unauthorized, ValueSum> {
let (balances, actions): (Vec<ValueSum>, Vec<Action<_>>) = acts.into_iter().unzip();
let flags = flags_for_version(bundle_version, flags);
Bundle::from_parts(
NonEmpty::from_vec(actions).unwrap(),
flags,
balances.into_iter().sum::<Result<ValueSum, _>>().unwrap(),
anchor,
super::EffectsOnly,
bundle_version,
)
.expect("flags are normalized to be representable under bundle_version")
}
}
prop_compose! {
pub fn arb_bundle(n_actions: usize)
(
bundle_version in arb_bundle_version(),
flags in arb_flags(),
)
(
acts in vec(arb_action_n(bundle_version.note_version(), n_actions, flags), n_actions),
anchor in arb_base().prop_map(Anchor::from),
sk in arb_binding_signing_key(),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_proof in vec(prop::num::u8::ANY, Proof::expected_proof_size(n_actions)),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY),
flags in Just(flags),
bundle_version in Just(bundle_version),
) -> Bundle<Authorized, ValueSum> {
let (balances, actions): (Vec<ValueSum>, Vec<Action<_>>) = acts.into_iter().unzip();
let rng = StdRng::from_seed(rng_seed);
let flags = flags_for_version(bundle_version, flags);
Bundle::try_from_parts(
NonEmpty::from_vec(actions).unwrap(),
flags,
balances.into_iter().sum::<Result<ValueSum, _>>().unwrap(),
anchor,
Authorized {
proof: Proof::new(fake_proof),
binding_signature: sk.sign(rng, &fake_sighash),
},
bundle_version,
)
.expect("fake proof has the canonical length and flags are representable")
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use alloc::vec;
use proptest::prelude::*;
use super::testing::{arb_bundle, arb_flags, arb_flags_ironwood_post_nu6_3};
use super::{
Authorized, Bundle, BundleError, BundleVersion, CommitmentError, Flags, TxVersion,
};
use crate::Proof;
#[cfg(feature = "circuit")]
pub(crate) fn with_cross_address_disabled(
bundle: Bundle<Authorized, crate::value::ValueSum>,
) -> Bundle<Authorized, crate::value::ValueSum> {
let mut flags = *bundle.flags();
flags.cross_address_enabled = false;
Bundle::from_parts_unchecked(
bundle.actions().clone(),
flags,
*bundle.value_balance(),
*bundle.anchor(),
bundle.authorization().clone(),
bundle.bundle_version(),
)
}
#[cfg(feature = "circuit")]
pub(crate) fn sample_authorized_bundle(
n_actions: usize,
) -> Bundle<Authorized, crate::value::ValueSum> {
use proptest::strategy::ValueTree;
let mut runner = proptest::test_runner::TestRunner::deterministic();
arb_bundle(n_actions)
.new_tree(&mut runner)
.expect("strategy can generate a bundle")
.current()
}
#[test]
fn flags_byte_encoding() {
for (flags, orchard_pre_nu6_3, orchard_nu6_3, ironwood_nu6_3) in [
(Flags::ENABLED, Some(0b011), None, Some(0b111)),
(Flags::SPENDS_DISABLED, Some(0b010), None, Some(0b110)),
(Flags::OUTPUTS_DISABLED, Some(0b001), None, Some(0b101)),
(
Flags::CROSS_ADDRESS_DISABLED,
None,
Some(0b011),
Some(0b011),
),
] {
assert_eq!(
flags.to_byte(BundleVersion::orchard_v2()),
orchard_pre_nu6_3
);
assert_eq!(flags.to_byte(BundleVersion::orchard_v3()), orchard_nu6_3);
assert_eq!(flags.to_byte(BundleVersion::ironwood_v3()), ironwood_nu6_3);
}
}
#[test]
fn flags_parsing_diverges_between_epochs() {
for value in 0b000..=0b011 {
let pre_nu6_3_flags = Flags::from_byte(value, BundleVersion::orchard_v2()).unwrap();
let nu6_3_flags = Flags::from_byte(value, BundleVersion::orchard_v3()).unwrap();
let ironwood_flags = Flags::from_byte(value, BundleVersion::ironwood_v3()).unwrap();
assert_eq!(
pre_nu6_3_flags.spends_enabled(),
nu6_3_flags.spends_enabled()
);
assert_eq!(
pre_nu6_3_flags.outputs_enabled(),
nu6_3_flags.outputs_enabled()
);
assert!(pre_nu6_3_flags.cross_address_enabled());
assert!(!nu6_3_flags.cross_address_enabled());
assert_eq!(ironwood_flags, nu6_3_flags);
assert_eq!(
pre_nu6_3_flags.to_byte(BundleVersion::orchard_v2()),
Some(value)
);
assert_eq!(
nu6_3_flags.to_byte(BundleVersion::orchard_v3()),
Some(value)
);
assert_eq!(nu6_3_flags.to_byte(BundleVersion::orchard_v2()), None);
}
assert_eq!(
Flags::from_byte(0b011, BundleVersion::orchard_v2()),
Some(Flags::ENABLED)
);
assert_eq!(
Flags::from_byte(0b011, BundleVersion::orchard_v3()),
Some(Flags::CROSS_ADDRESS_DISABLED)
);
assert_eq!(
Flags::from_byte(0b011, BundleVersion::ironwood_v3()),
Some(Flags::CROSS_ADDRESS_DISABLED)
);
}
#[test]
fn only_orchard_post_nu6_3_requires_the_cross_address_restriction() {
assert!(!BundleVersion::orchard_v3().permits_cross_address_transfers());
for bundle_version in [
BundleVersion::orchard_insecure_v1(),
BundleVersion::orchard_v2(),
BundleVersion::ironwood_v3(),
] {
assert!(bundle_version.permits_cross_address_transfers());
}
}
#[test]
fn pre_nu6_3_flags_parsing_rejects_reserved_bits() {
for value in 0b100..=u8::MAX {
assert_eq!(Flags::from_byte(value, BundleVersion::orchard_v2()), None);
}
}
#[test]
fn nu6_3_flags_parsing_handles_the_cross_address_bit() {
for value in 0b100..=0b111 {
assert_eq!(Flags::from_byte(value, BundleVersion::orchard_v3()), None);
let flags = Flags::from_byte(value, BundleVersion::ironwood_v3()).unwrap();
assert!(flags.cross_address_enabled());
assert_eq!(flags.to_byte(BundleVersion::ironwood_v3()), Some(value));
assert_eq!(
flags.to_byte(BundleVersion::orchard_v2()),
Some(value & 0b011)
);
}
for value in 0b1000..=u8::MAX {
assert_eq!(Flags::from_byte(value, BundleVersion::orchard_v3()), None);
assert_eq!(Flags::from_byte(value, BundleVersion::ironwood_v3()), None);
}
}
#[test]
fn expected_proof_size_matches_known_values() {
assert_eq!(Proof::expected_proof_size(1), 4992);
assert_eq!(Proof::expected_proof_size(2), 7264);
let per_action = Proof::expected_proof_size(2) - Proof::expected_proof_size(1);
assert_eq!(
Proof::expected_proof_size(3) - Proof::expected_proof_size(2),
per_action,
);
}
#[test]
fn empty_commitments_are_domain_separated() {
use crate::bundle::commitments::{hash_bundle_auth_empty, hash_bundle_txid_empty};
use crate::ValuePool;
let formats = [
(ValuePool::Orchard, TxVersion::V5),
(ValuePool::Orchard, TxVersion::V6),
(ValuePool::Ironwood, TxVersion::V6),
];
for i in 0..formats.len() {
for j in (i + 1)..formats.len() {
let (pi, ti) = formats[i];
let (pj, tj) = formats[j];
assert_ne!(
hash_bundle_txid_empty(pi, ti).unwrap().as_bytes(),
hash_bundle_txid_empty(pj, tj).unwrap().as_bytes()
);
assert_ne!(
hash_bundle_auth_empty(pi, ti).unwrap().as_bytes(),
hash_bundle_auth_empty(pj, tj).unwrap().as_bytes()
);
}
}
assert!(matches!(
hash_bundle_txid_empty(ValuePool::Ironwood, TxVersion::V5),
Err(CommitmentError::InvalidTransactionVersion)
));
assert!(matches!(
hash_bundle_auth_empty(ValuePool::Ironwood, TxVersion::V5),
Err(CommitmentError::InvalidTransactionVersion)
));
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(16))]
#[test]
fn arb_flags_ironwood_post_nu6_3_round_trips(flags in arb_flags_ironwood_post_nu6_3()) {
let encoded = flags
.to_byte(BundleVersion::ironwood_v3())
.expect("all Ironwood post-NU6.3 flag strategy outputs encode under Ironwood post-NU6.3");
prop_assert_eq!(Flags::from_byte(encoded, BundleVersion::ironwood_v3()), Some(flags));
}
#[test]
fn orchard_nu6_3_rejects_cross_address_enabled(flags in arb_flags()) {
prop_assert_eq!(flags.to_byte(BundleVersion::orchard_v3()), None);
let mut disabled = flags;
disabled.cross_address_enabled = false;
let encoded = disabled
.to_byte(BundleVersion::orchard_v3())
.expect("cross-address-disabled flags encode under Orchard post-NU6.3");
prop_assert_eq!(
Flags::from_byte(encoded, BundleVersion::orchard_v3()),
Some(disabled)
);
}
#[test]
fn commitment_hashes_the_wire_flag_byte(bundle in arb_bundle(3)) {
let actions = bundle.actions().clone();
let anchor = *bundle.anchor();
let authorization = bundle.authorization().clone();
let spends_enabled = bundle.flags().spends_enabled();
let outputs_enabled = bundle.flags().outputs_enabled();
let enabled = Flags::from_parts(spends_enabled, outputs_enabled, true);
let disabled = Flags::from_parts(spends_enabled, outputs_enabled, false);
let build = |flags, bundle_version| {
Bundle::from_parts_unchecked(
actions.clone(),
flags,
0i64,
anchor,
authorization.clone(),
bundle_version,
)
};
let legacy = build(enabled, BundleVersion::orchard_v2());
let restricted = build(disabled, BundleVersion::orchard_v3());
prop_assert_eq!(restricted.flag_byte(), legacy.flag_byte());
let restricted_commitment: [u8; 32] =
restricted.commitment(TxVersion::V5).unwrap().into();
let legacy_commitment: [u8; 32] = legacy.commitment(TxVersion::V5).unwrap().into();
prop_assert_eq!(restricted_commitment, legacy_commitment);
let unrestricted = build(enabled, BundleVersion::ironwood_v3());
let unrestricted_commitment: [u8; 32] =
unrestricted.commitment(TxVersion::V6).unwrap().into();
prop_assert_ne!(unrestricted_commitment, restricted_commitment);
prop_assert_eq!(disabled.to_byte(BundleVersion::orchard_v2()), None);
}
#[test]
fn ironwood_rejects_v5_commitment_version(bundle in arb_bundle(3)) {
let bundle_i64 = Bundle::from_parts_unchecked(
bundle.actions().clone(),
*bundle.flags(),
0i64,
*bundle.anchor(),
bundle.authorization().clone(),
BundleVersion::ironwood_v3(),
);
let ironwood = Bundle::from_parts_unchecked(
bundle.actions().clone(),
*bundle.flags(),
*bundle.value_balance(),
*bundle.anchor(),
bundle.authorization().clone(),
BundleVersion::ironwood_v3(),
);
prop_assert!(matches!(
bundle_i64.commitment(TxVersion::V5),
Err(CommitmentError::InvalidTransactionVersion)
));
prop_assert!(matches!(
ironwood.authorizing_commitment(TxVersion::V5),
Err(CommitmentError::InvalidTransactionVersion)
));
}
#[test]
fn anchor_placement_follows_tx_version(bundle in arb_bundle(3)) {
let flags = Flags::from_parts(
bundle.flags().spends_enabled(),
bundle.flags().outputs_enabled(),
false,
);
let with_anchor = |anchor, bundle_version| {
Bundle::from_parts_unchecked(
bundle.actions().clone(),
flags,
0i64,
anchor,
bundle.authorization().clone(),
bundle_version,
)
};
let anchor_a = crate::Anchor::from_bytes([0u8; 32]).unwrap();
let anchor_b = crate::Anchor::from_bytes([6u8; 32]).unwrap();
for (bundle_version, tx, anchor_in_txid_digest) in [
(BundleVersion::orchard_v3(), TxVersion::V5, true),
(BundleVersion::orchard_v3(), TxVersion::V6, false),
(BundleVersion::ironwood_v3(), TxVersion::V6, false),
] {
let a = with_anchor(anchor_a, bundle_version);
let b = with_anchor(anchor_b, bundle_version);
let txid_a: [u8; 32] = a.commitment(tx).unwrap().into();
let txid_b: [u8; 32] = b.commitment(tx).unwrap().into();
let auth_a = a.authorizing_commitment(tx).unwrap().0;
let auth_b = b.authorizing_commitment(tx).unwrap().0;
if anchor_in_txid_digest {
prop_assert_ne!(txid_a, txid_b);
prop_assert_eq!(auth_a.as_bytes(), auth_b.as_bytes());
} else {
prop_assert_eq!(txid_a, txid_b);
prop_assert_ne!(auth_a.as_bytes(), auth_b.as_bytes());
}
}
let a_v2 = with_anchor(anchor_a, BundleVersion::orchard_v3());
let orchard_v5: [u8; 32] = a_v2.commitment(TxVersion::V5).unwrap().into();
let orchard_v6: [u8; 32] = a_v2.commitment(TxVersion::V6).unwrap().into();
prop_assert_ne!(orchard_v5, orchard_v6);
}
#[test]
fn try_from_parts_enforces_canonical_proof_size(
bundle in arb_bundle(3)
) {
let actions = bundle.actions().clone();
let expected = Proof::expected_proof_size(actions.len());
let flags = *bundle.flags();
let bundle_version = BundleVersion::ironwood_v3();
let value_balance = *bundle.value_balance();
let anchor = *bundle.anchor();
let binding_signature = bundle.authorization().binding_signature().clone();
let with_proof_len = |proof_len: usize| {
Bundle::try_from_parts(
actions.clone(),
flags,
value_balance,
anchor,
Authorized::from_parts(
Proof::new(vec![0u8; proof_len]),
binding_signature.clone(),
),
bundle_version,
)
};
prop_assert!(with_proof_len(expected).is_ok());
prop_assert_eq!(
with_proof_len(expected + 1).err(),
Some(BundleError::NonCanonicalProofSize { expected, actual: expected + 1 })
);
prop_assert_eq!(
with_proof_len(expected - 1).err(),
Some(BundleError::NonCanonicalProofSize { expected, actual: expected - 1 })
);
}
#[test]
fn try_from_parts_preserves_cross_address_disabled(
bundle in arb_bundle(3)
) {
let actions = bundle.actions().clone();
let mut flags = *bundle.flags();
flags.cross_address_enabled = false;
let value_balance = *bundle.value_balance();
let anchor = *bundle.anchor();
let authorization = bundle.authorization().clone();
let bundle = Bundle::try_from_parts(
actions,
flags,
value_balance,
anchor,
authorization,
BundleVersion::orchard_v3(),
)
.expect("canonical proof size is accepted");
prop_assert!(!bundle.flags().cross_address_enabled());
}
#[test]
fn try_from_parts_checks_proof_size_with_cross_address_disabled(
bundle in arb_bundle(3)
) {
let actions = bundle.actions().clone();
let expected = Proof::expected_proof_size(actions.len());
let mut flags = *bundle.flags();
flags.cross_address_enabled = false;
let value_balance = *bundle.value_balance();
let anchor = *bundle.anchor();
let binding_signature = bundle.authorization().binding_signature().clone();
prop_assert_eq!(
Bundle::try_from_parts(
actions,
flags,
value_balance,
anchor,
Authorized::from_parts(
Proof::new(vec![0u8; expected + 1]),
binding_signature,
),
BundleVersion::orchard_v3(),
)
.err(),
Some(BundleError::NonCanonicalProofSize { expected, actual: expected + 1 })
);
}
#[test]
fn insecure_v1_skips_proof_size_enforcement(bundle in arb_bundle(3)) {
let expected = Proof::expected_proof_size(bundle.actions().len());
let padded = Bundle::try_from_parts(
bundle.actions().clone(),
Flags::ENABLED,
*bundle.value_balance(),
*bundle.anchor(),
Authorized::from_parts(
Proof::new(vec![0u8; expected + 1]),
bundle.authorization().binding_signature().clone(),
),
BundleVersion::orchard_insecure_v1(),
);
prop_assert!(padded.is_ok());
}
}
#[cfg(feature = "circuit")]
#[test]
fn from_parts_rejects_unrepresentable_flags() {
let bundle = sample_authorized_bundle(1);
let flags = Flags::from_parts(
bundle.flags().spends_enabled(),
bundle.flags().outputs_enabled(),
false,
);
assert_eq!(
Bundle::try_from_parts(
bundle.actions().clone(),
flags,
*bundle.value_balance(),
*bundle.anchor(),
bundle.authorization().clone(),
BundleVersion::orchard_v2(),
)
.err(),
Some(BundleError::UnrepresentableFlags)
);
}
#[cfg(feature = "circuit")]
#[test]
fn verify_proof_rejects_cross_address_disabled_for_unsupported_keys() {
let bundle = with_cross_address_disabled(sample_authorized_bundle(1));
for circuit_version in [
crate::circuit::OrchardCircuitVersion::InsecurePreNu6_2,
crate::circuit::OrchardCircuitVersion::FixedPostNu6_2,
] {
let vk = crate::circuit::VerifyingKey::build(circuit_version);
assert!(matches!(
bundle.verify_proof(&vk),
Err(halo2_proofs::plonk::Error::InvalidInstances)
));
}
}
}