use group::{
ff::{FromUniformBytes, PrimeField},
CurveAffine, GroupEncoding,
};
use halo2::pasta::pallas;
use nonempty::NonEmpty;
use orchard::{
bundle::{Authorized, BundleVersion, Flags},
note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext},
note_encryption::NoteBytesData,
primitives::redpallas::{self, SpendAuth},
value::ValueCommitment,
Action, Anchor, Bundle, Proof,
};
use zcash_protocol::value::ZatBalance;
fn base_from_seed(seed: u64) -> pallas::Base {
let mut bytes = [0u8; 64];
bytes[..8].copy_from_slice(&seed.to_le_bytes());
bytes[63] = 0x01;
pallas::Base::from_uniform_bytes(&bytes)
}
fn scalar_from_seed(seed: u64) -> pallas::Scalar {
let mut bytes = [0u8; 64];
bytes[..8].copy_from_slice(&seed.to_le_bytes());
bytes[63] = 0x02;
pallas::Scalar::from_uniform_bytes(&bytes)
}
fn verification_key_from_seed(seed: u64) -> redpallas::VerificationKey<SpendAuth> {
let sk_bytes = scalar_from_seed(seed).to_repr();
let sk = reddsa::SigningKey::<reddsa::orchard::SpendAuth>::from_bytes(&sk_bytes)
.expect("a canonical scalar is a valid signing key");
let pk_bytes: [u8; 32] = reddsa::VerificationKey::from(&sk).into();
redpallas::VerificationKey::try_from(pk_bytes)
.expect("a key derived from a signing key is a valid, non-identity verification key")
}
fn fake_action(seed: u64) -> Action<redpallas::Signature<SpendAuth>> {
let cv_net = ValueCommitment::from_bytes(&pallas::Affine::identity().to_bytes())
.expect("the identity point is a canonical value commitment encoding");
let nullifier = Nullifier::from_bytes(&base_from_seed(seed).to_repr())
.expect("a canonical base field element is a valid nullifier");
let cmx = ExtractedNoteCommitment::from_bytes(&base_from_seed(seed ^ 0xFFFF).to_repr())
.expect("a canonical base field element is a valid note commitment");
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: pallas::Affine::generator().to_bytes(),
enc_ciphertext: NoteBytesData([0u8; 580]),
out_ciphertext: [0u8; 80],
};
let spend_auth_sig = redpallas::Signature::<SpendAuth>::from([0u8; 64]);
Action::from_parts(
nullifier,
verification_key_from_seed(seed),
cmx,
encrypted_note,
cv_net,
spend_auth_sig,
)
.expect("action parts are valid: rk and epk are non-identity")
}
pub fn fake_orchard_bundle(
flags: Flags,
value_balance: ZatBalance,
n_actions: usize,
seed: u64,
bundle_version: BundleVersion,
) -> Bundle<Authorized, ZatBalance> {
assert!(n_actions > 0, "an Orchard bundle must have some actions");
let actions: Vec<_> = (0..n_actions)
.map(|i| fake_action(seed.wrapping_add(i as u64)))
.collect();
let authorization = Authorized::from_parts(
Proof::new(vec![0u8; Proof::expected_proof_size(n_actions)]),
redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
);
Bundle::try_from_parts(
NonEmpty::from_vec(actions).expect("n_actions is non-zero"),
flags,
value_balance,
Anchor::from(base_from_seed(seed ^ 0xA11C)),
authorization,
bundle_version,
)
.expect("the proof has the canonical length and the flags are representable")
}
pub fn fake_orchard_bundle_duplicate_nullifiers(
flags: Flags,
value_balance: ZatBalance,
n_actions: usize,
seed: u64,
bundle_version: BundleVersion,
) -> Bundle<Authorized, ZatBalance> {
assert!(n_actions > 0, "an Orchard bundle must have some actions");
let actions: Vec<_> = (0..n_actions).map(|_| fake_action(seed)).collect();
let authorization = Authorized::from_parts(
Proof::new(vec![0u8; Proof::expected_proof_size(n_actions)]),
redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
);
Bundle::try_from_parts(
NonEmpty::from_vec(actions).expect("n_actions is non-zero"),
flags,
value_balance,
Anchor::from(base_from_seed(seed ^ 0xA11C)),
authorization,
bundle_version,
)
.expect("the proof has the canonical length and the flags are representable")
}
const NOTE_VECTOR_SEED: u64 = 0x4E4F_5445;
#[allow(clippy::too_many_arguments)]
pub fn fake_orchard_bundle_with_note(
flags: Flags,
value_balance: ZatBalance,
bundle_version: BundleVersion,
cv_net: &[u8; 32],
nullifier: &[u8; 32],
cmx: &[u8; 32],
epk_bytes: [u8; 32],
enc_ciphertext: [u8; 580],
out_ciphertext: [u8; 80],
) -> Bundle<Authorized, ZatBalance> {
let action = Action::from_parts(
Nullifier::from_bytes(nullifier).expect("the test vector's rho is a valid nullifier"),
verification_key_from_seed(NOTE_VECTOR_SEED),
ExtractedNoteCommitment::from_bytes(cmx)
.expect("the test vector's cmx is a valid note commitment"),
TransmittedNoteCiphertext {
epk_bytes,
enc_ciphertext: NoteBytesData(enc_ciphertext),
out_ciphertext,
},
ValueCommitment::from_bytes(cv_net).expect("the test vector's cv_net is a valid point"),
redpallas::Signature::<SpendAuth>::from([0u8; 64]),
)
.expect("the test vector supplies a non-identity rk and ephemeral key");
let authorization = Authorized::from_parts(
Proof::new(vec![0u8; Proof::expected_proof_size(1)]),
redpallas::Signature::<redpallas::Binding>::from([0u8; 64]),
);
Bundle::try_from_parts(
NonEmpty::from_vec(vec![action]).expect("exactly one action"),
flags,
value_balance,
Anchor::from(base_from_seed(0xA11C)),
authorization,
bundle_version,
)
.expect("the proof has the canonical length and the flags are representable")
}
pub fn outputs_enabled_flags(bundle_version: BundleVersion) -> Flags {
if Flags::ENABLED.to_byte(bundle_version).is_some() {
Flags::ENABLED
} else {
Flags::CROSS_ADDRESS_DISABLED
}
}
pub fn fake_bundle_for_branch(
branch_id: zcash_protocol::consensus::BranchId,
pool: ::orchard::ValuePool,
n_actions: usize,
seed: u64,
) -> Option<Bundle<Authorized, ZatBalance>> {
let bundle_version =
zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
branch_id, pool,
)?;
let flags = if Flags::ENABLED.to_byte(bundle_version).is_some() {
Flags::ENABLED
} else {
Flags::CROSS_ADDRESS_DISABLED
};
Some(fake_orchard_bundle(
flags,
ZatBalance::from_i64(0).expect("zero is a valid balance"),
n_actions,
seed,
bundle_version,
))
}
pub const ACTION_WIRE_SIZE: usize = 32 * 5 + 580 + 80;
pub fn v6_orchard_flags_offset(n_actions: usize) -> usize {
assert!(n_actions < 253, "n_actions must be a one-byte CompactSize");
(4 * 5) + 2 + 2 + 1 + n_actions * ACTION_WIRE_SIZE
}
pub fn v6_ironwood_flags_offset(n_ironwood_actions: usize) -> usize {
assert!(
n_ironwood_actions < 253,
"n_ironwood_actions must be a one-byte CompactSize"
);
(4 * 5) + 2 + 2 + 1 + 1 + n_ironwood_actions * ACTION_WIRE_SIZE
}
pub const V5_FIRST_ACTION_RK_OFFSET: usize = (4 * 5) + 2 + 2 + 1 + 32 + 32;
pub fn fake_v6_transaction(
network_upgrade: crate::parameters::NetworkUpgrade,
orchard_bundle: Option<Bundle<Authorized, ZatBalance>>,
ironwood_bundle: Option<Bundle<Authorized, ZatBalance>>,
) -> crate::transaction::Transaction {
crate::transaction::Transaction::test_v6_with_bundles(
network_upgrade,
Vec::new(),
Vec::new(),
crate::transaction::LockTime::min_lock_time_timestamp(),
crate::block::Height(0),
orchard_bundle,
ironwood_bundle,
)
}
pub fn with_orchard_value_balance(
tx: crate::transaction::Transaction,
value_balance: i64,
) -> crate::transaction::Transaction {
let balance = ZatBalance::from_i64(value_balance).expect("a valid signed amount");
let bundle = tx
.orchard_bundle()
.expect("the transaction must have an Orchard bundle")
.clone()
.try_map_value_balance::<_, (), _>(|_| Ok(balance))
.expect("the mapping cannot fail");
tx.with_orchard_bundle(Some(bundle))
}
pub fn insert_fake_orchard_shielded_data(
tx: crate::transaction::Transaction,
) -> crate::transaction::Transaction {
let branch_id = tx.inner().consensus_branch_id();
let bundle = fake_bundle_for_branch(branch_id, ::orchard::ValuePool::Orchard, 1, 0xF00D)
.expect("the Orchard pool is defined for this transaction's branch");
tx.with_orchard_bundle(Some(bundle))
}
pub fn with_orchard_flags(
tx: crate::transaction::Transaction,
flags: Flags,
) -> crate::transaction::Transaction {
let bundle = tx
.orchard_bundle()
.expect("the transaction must have an Orchard bundle");
let rebuilt = Bundle::try_from_parts(
bundle.actions().clone(),
flags,
*bundle.value_balance(),
*bundle.anchor(),
bundle.authorization().clone(),
bundle.bundle_version(),
)
.expect("the flags must be representable under the bundle version");
tx.with_orchard_bundle(Some(rebuilt))
}
pub fn with_garbage_orchard_authorization(
tx: crate::transaction::Transaction,
) -> crate::transaction::Transaction {
let bundle = tx
.orchard_bundle()
.expect("the transaction must have an Orchard bundle");
let actions: Vec<_> = bundle
.actions()
.iter()
.map(|action| {
Action::from_parts(
*action.nullifier(),
action.rk().clone(),
*action.cmx(),
TransmittedNoteCiphertext {
epk_bytes: action.encrypted_note().epk_bytes,
enc_ciphertext: action.encrypted_note().enc_ciphertext,
out_ciphertext: action.encrypted_note().out_ciphertext,
},
action.cv_net().clone(),
redpallas::Signature::<SpendAuth>::from([0xFF; 64]),
)
.expect("the effects are copied from a valid action")
})
.collect();
let authorization = Authorized::from_parts(
Proof::new(vec![0xDE, 0xAD, 0xBE, 0xEF]),
redpallas::Signature::<redpallas::Binding>::from([0xFF; 64]),
);
let rebuilt = Bundle::try_from_parts(
NonEmpty::from_vec(actions).expect("the source bundle is non-empty"),
*bundle.flags(),
*bundle.value_balance(),
*bundle.anchor(),
authorization,
bundle.bundle_version(),
)
.expect("the bundle version must not enforce a canonical proof size");
tx.with_orchard_bundle(Some(rebuilt))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
parameters::NetworkUpgrade,
serialization::{ZcashDeserializeInto, ZcashSerialize},
transaction::Transaction,
};
#[test]
fn fake_bundles_round_trip() {
let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
let orchard = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
2,
1,
BundleVersion::orchard_v3(),
);
let ironwood =
fake_orchard_bundle(Flags::ENABLED, zero, 1, 1000, BundleVersion::ironwood_v3());
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), Some(ironwood));
assert_eq!(tx.orchard_actions().count(), 2);
assert_eq!(tx.ironwood_actions().count(), 1);
let bytes = tx
.zcash_serialize_to_vec()
.expect("a v6 transaction with fake bundles serializes");
let tx2: Transaction = bytes
.zcash_deserialize_into()
.expect("a v6 transaction with fake bundles deserializes");
assert_eq!(tx.hash(), tx2.hash());
assert_eq!(tx2.orchard_actions().count(), 2);
assert_eq!(tx2.ironwood_actions().count(), 1);
}
#[test]
fn fake_bundles_have_distinct_nullifiers() {
let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
let a = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
3,
0,
BundleVersion::orchard_v3(),
);
let b = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
3,
100,
BundleVersion::orchard_v3(),
);
let nfs = |bundle: &Bundle<Authorized, ZatBalance>| {
bundle
.actions()
.iter()
.map(|action| action.nullifier().to_bytes())
.collect::<Vec<_>>()
};
let (a_nfs, b_nfs) = (nfs(&a), nfs(&b));
let mut sorted = a_nfs.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
3,
"actions in a bundle have distinct nullifiers"
);
for nf in &a_nfs {
assert!(
!b_nfs.contains(nf),
"bundles from different seeds must not collide"
);
}
}
}