Merge pull request #65 from nuttycom/zip_225_updates

Update Orchard data structures to support ZIP 225 serialization & property testing.

Fixes #34
This commit is contained in:
Kris Nuttycombe 2021-05-06 11:46:27 -06:00 committed by GitHub
commit 3117187280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 768 additions and 86 deletions

View File

@ -25,6 +25,7 @@ blake2b_simd = "0.5"
ff = "0.9"
fpe = "0.4"
group = "0.9"
proptest = { version = "1.0.0", optional = true }
rand = "0.8"
rand_7 = { package = "rand", version = "0.7" }
nonempty = "0.6"
@ -50,6 +51,9 @@ proptest = "1.0.0"
[lib]
bench = false
[features]
test-dependencies = ["proptest"]
[[bench]]
name = "small"
harness = false

View File

@ -15,7 +15,7 @@ use crate::{
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
/// let address = FullViewingKey::from(&sk).default_address();
/// ```
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Address {
d: Diversifier,
pk_d: DiversifiedTransmissionKey,
@ -38,3 +38,21 @@ impl Address {
&self.pk_d
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use crate::keys::{testing::arb_spending_key, FullViewingKey};
use super::Address;
prop_compose! {
/// Generates an arbitrary payment address.
pub(crate) fn arb_address()(sk in arb_spending_key()) -> Address {
let fvk = FullViewingKey::from(&sk);
fvk.default_address()
}
}
}

View File

@ -1,5 +1,6 @@
//! Logic for building Orchard components of transactions.
use std::convert::TryFrom;
use std::iter;
use ff::Field;
@ -8,23 +9,29 @@ use pasta_curves::pallas;
use rand::RngCore;
use crate::{
address::Address,
bundle::{Action, Authorization, Authorized, Bundle, Flags},
circuit::{Circuit, Proof, ProvingKey},
keys::{
FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey,
},
note::{Note, TransmittedNoteCiphertext},
primitives::redpallas::{self, Binding, SpendAuth},
tree::{Anchor, MerklePath},
value::{self, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum},
Address, EncryptedNote, Note,
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
};
const MIN_ACTIONS: usize = 2;
/// An error type for the kinds of errors that can occur during bundle construction.
#[derive(Debug)]
pub enum Error {
/// A bundle could not be built because required signatures were missing.
MissingSignatures,
/// An error occurred in the process of producing a proof for a bundle.
Proof(halo2::plonk::Error),
/// An overflow error occurred while attempting to construct the value
/// for a bundle.
ValueSum(value::OverflowError),
}
@ -110,7 +117,7 @@ impl ActionInfo {
}
/// Returns the value sum for this action.
fn value_sum(&self) -> Result<ValueSum, value::OverflowError> {
fn value_sum(&self) -> Option<ValueSum> {
self.spend.note.value() - self.output.value
}
@ -137,7 +144,11 @@ impl ActionInfo {
let cm_new = note.commitment();
// TODO: Note encryption
let encrypted_note = EncryptedNote;
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
out_ciphertext: [0u8; 80],
};
(
Action::from_parts(
@ -158,6 +169,7 @@ impl ActionInfo {
/// A builder that constructs a [`Bundle`] from a set of notes to be spent, and recipients
/// to receive funds.
#[derive(Debug)]
pub struct Builder {
spends: Vec<SpendInfo>,
recipients: Vec<RecipientInfo>,
@ -166,6 +178,7 @@ pub struct Builder {
}
impl Builder {
/// Constructs a new empty builder for an Orchard bundle.
pub fn new(flags: Flags, anchor: Anchor) -> Self {
Builder {
spends: vec![],
@ -233,11 +246,11 @@ impl Builder {
///
/// This API assumes that none of the notes being spent are controlled by (threshold)
/// multisignatures, and immediately constructs the bundle proof.
fn build(
fn build<V: TryFrom<i64>>(
mut self,
mut rng: impl RngCore,
pk: &ProvingKey,
) -> Result<Bundle<Unauthorized>, Error> {
) -> Result<Bundle<Unauthorized, V>, Error> {
// Pair up the spends and recipients, extending with dummy values as necessary.
//
// TODO: Do we want to shuffle the order like we do for Sapling? And if we do, do
@ -272,11 +285,16 @@ impl Builder {
let anchor = self.anchor;
// Determine the value balance for this bundle, ensuring it is valid.
let value_balance: ValueSum = pre_actions
let value_balance = pre_actions
.iter()
.fold(Ok(ValueSum::zero()), |acc, action| {
.fold(Some(ValueSum::zero()), |acc, action| {
acc? + action.value_sum()?
})?;
})
.ok_or(OverflowError)?;
let result_value_balance: V = i64::try_from(value_balance)
.map_err(Error::ValueSum)
.and_then(|i| V::try_from(i).map_err(|_| Error::ValueSum(value::OverflowError)))?;
// Compute the transaction binding signing key.
let bsk = pre_actions
@ -305,7 +323,7 @@ impl Builder {
Ok(Bundle::from_parts(
NonEmpty::from_vec(actions).unwrap(),
flags,
value_balance,
result_value_balance,
anchor,
Unauthorized { proof, bsk },
))
@ -349,7 +367,7 @@ impl Authorization for PartiallyAuthorized {
type SpendAuth = (Option<redpallas::Signature<SpendAuth>>, SpendValidatingKey);
}
impl Bundle<Unauthorized> {
impl<V> Bundle<Unauthorized, V> {
/// Loads the sighash into this bundle, preparing it for signing.
///
/// This API ensures that all signatures are created over the same sighash.
@ -357,7 +375,7 @@ impl Bundle<Unauthorized> {
self,
mut rng: R,
sighash: [u8; 32],
) -> Bundle<PartiallyAuthorized> {
) -> Bundle<PartiallyAuthorized, V> {
self.authorize(
&mut rng,
|rng, _, SigningMetadata { dummy_ask, ak }| {
@ -381,7 +399,7 @@ impl Bundle<Unauthorized> {
mut rng: R,
sighash: [u8; 32],
signing_keys: &[SpendAuthorizingKey],
) -> Result<Bundle<Authorized>, Error> {
) -> Result<Bundle<Authorized, V>, Error> {
signing_keys
.iter()
.fold(self.prepare(&mut rng, sighash), |partial, ask| {
@ -391,7 +409,7 @@ impl Bundle<Unauthorized> {
}
}
impl Bundle<PartiallyAuthorized> {
impl<V> Bundle<PartiallyAuthorized, V> {
/// Signs this bundle with the given [`SpendAuthorizingKey`].
///
/// This will apply signatures for all notes controlled by this spending key.
@ -422,7 +440,7 @@ impl Bundle<PartiallyAuthorized> {
/// Finalizes this bundle, enabling it to be included in a transaction.
///
/// Returns an error if any signatures are missing.
pub fn finalize(self) -> Result<Bundle<Authorized>, Error> {
pub fn finalize(self) -> Result<Bundle<Authorized, V>, Error> {
self.try_authorize(
&mut (),
|_, _, (sig, _)| match sig {
@ -439,17 +457,139 @@ impl Bundle<PartiallyAuthorized> {
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use rand::{rngs::StdRng, CryptoRng, SeedableRng};
use std::convert::TryFrom;
use std::fmt::Debug;
use proptest::collection::vec;
use proptest::prelude::*;
use crate::{
address::testing::arb_address,
bundle::{Authorized, Bundle, Flags},
circuit::ProvingKey,
keys::{
testing::arb_spending_key, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey,
SpendingKey,
},
note::testing::arb_note,
tree::{Anchor, MerklePath},
value::{testing::arb_positive_note_value, NoteValue, MAX_NOTE_VALUE},
Address, Note,
};
use super::Builder;
/// An intermediate type used for construction of arbitrary
/// bundle values. This type is required because of a limitation
/// of the proptest prop_compose! macro which does not correctly
/// handle polymorphic generator functions. Instead of generating
/// a bundle directly, we generate the bundle inputs, and then
/// are able to use the `build` function to construct the bundle
/// from these inputs, but using a `ValueBalance` implementation that
/// is defined by the end user.
#[derive(Debug)]
struct ArbitraryBundleInputs<R, R7> {
rng: R,
rng_7: R7,
sk: SpendingKey,
anchor: Anchor,
notes: Vec<Note>,
recipient_amounts: Vec<(Address, NoteValue)>,
}
impl<R: RngCore + CryptoRng, R7: rand_7::RngCore + rand_7::CryptoRng> ArbitraryBundleInputs<R, R7> {
/// Create a bundle from the set of arbitrary bundle inputs.
fn into_bundle<V: TryFrom<i64>>(mut self) -> Bundle<Authorized, V> {
let fvk = FullViewingKey::from(&self.sk);
let ovk = OutgoingViewingKey::from(&fvk);
let flags = Flags::from_parts(true, true);
let mut builder = Builder::new(flags, self.anchor);
for note in self.notes.into_iter() {
builder.add_spend(fvk.clone(), note, MerklePath).unwrap();
}
for (addr, value) in self.recipient_amounts.into_iter() {
builder
.add_recipient(Some(ovk.clone()), addr, value, None)
.unwrap();
}
let pk = ProvingKey::build();
builder
.build(&mut self.rng, &pk)
.unwrap()
.prepare(&mut self.rng_7, [0; 32])
.sign(&mut self.rng_7, &SpendAuthorizingKey::from(&self.sk))
.finalize()
.unwrap()
}
}
prop_compose! {
/// Produce a random valid Orchard bundle.
fn arb_bundle_inputs(sk: SpendingKey)
(
n_notes in 1..30,
n_recipients in 1..30,
)
(
anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor),
// generate note values that we're certain won't exceed MAX_NOTE_VALUE in total
notes in vec(
arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note),
n_notes as usize
),
recipient_amounts in vec(
arb_address().prop_flat_map(move |a| {
arb_positive_note_value(MAX_NOTE_VALUE / n_recipients as u64)
.prop_map(move |v| (a.clone(), v))
}),
n_recipients as usize
),
rng_seed in prop::array::uniform32(prop::num::u8::ANY)
) -> ArbitraryBundleInputs<StdRng, rand_7::rngs::StdRng> {
ArbitraryBundleInputs {
rng: StdRng::from_seed(rng_seed),
rng_7: <rand_7::rngs::StdRng as rand_7::SeedableRng>::from_seed(rng_seed),
sk: sk.clone(),
anchor,
notes,
recipient_amounts
}
}
}
/// Produce an arbitrary valid Orchard bundle using a random spending key.
pub fn arb_bundle<V: TryFrom<i64> + Debug>() -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_spending_key()
.prop_flat_map(arb_bundle_inputs)
.prop_map(|inputs| inputs.into_bundle::<V>())
}
/// Produce an arbitrary valid Orchard bundle using a specified spending key.
pub fn arb_bundle_with_key<V: TryFrom<i64> + Debug>(
k: SpendingKey,
) -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_bundle_inputs(k).prop_map(|inputs| inputs.into_bundle::<V>())
}
}
#[cfg(test)]
mod tests {
use rand::rngs::OsRng;
use super::Builder;
use crate::{
bundle::Flags,
bundle::{Authorized, Bundle, Flags},
circuit::ProvingKey,
keys::{FullViewingKey, SpendingKey},
tree::Anchor,
value::{NoteValue, ValueSum},
value::NoteValue,
};
#[test]
@ -461,16 +601,16 @@ mod tests {
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.default_address();
let mut builder = Builder::new(Flags::from_parts(true, true), Anchor);
let mut builder = Builder::new(Flags::from_parts(true, true), Anchor([0; 32]));
builder
.add_recipient(None, recipient, NoteValue::from_raw(5000), None)
.unwrap();
let bundle = dbg!(builder
let bundle: Bundle<Authorized, i64> = dbg!(builder
.build(&mut rng, &pk)
.unwrap()
.prepare(rand_7::rngs::OsRng, [0; 32]))
.finalize()
.unwrap();
assert_eq!(bundle.value_balance(), &ValueSum::from_raw(-5000))
assert_eq!(bundle.value_balance(), &(-5000))
}
}

View File

@ -4,10 +4,10 @@ use nonempty::NonEmpty;
use crate::{
circuit::{Instance, Proof},
note::{EncryptedNote, ExtractedNoteCommitment, Nullifier},
note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext},
primitives::redpallas::{self, Binding, SpendAuth},
tree::Anchor,
value::{ValueCommitment, ValueSum},
value::ValueCommitment,
};
/// An action applied to the global ledger.
@ -19,19 +19,19 @@ use crate::{
/// Internally, this may both consume a note and create a note, or it may do only one of
/// the two. TODO: Determine which is more efficient (circuit size vs bundle size).
#[derive(Debug)]
pub struct Action<T> {
pub struct Action<A> {
/// The nullifier of the note being spent.
nf: Nullifier,
/// The randomized verification key for the note being spent.
rk: redpallas::VerificationKey<SpendAuth>,
/// A commitment to the new note being created.
cmx: ExtractedNoteCommitment,
/// The encrypted output note.
encrypted_note: EncryptedNote,
/// The transmitted note ciphertext.
encrypted_note: TransmittedNoteCiphertext,
/// A commitment to the net value created or consumed by this action.
cv_net: ValueCommitment,
/// The authorization for this action.
authorization: T,
authorization: A,
}
impl<T> Action<T> {
@ -40,7 +40,7 @@ impl<T> Action<T> {
nf: Nullifier,
rk: redpallas::VerificationKey<SpendAuth>,
cmx: ExtractedNoteCommitment,
encrypted_note: EncryptedNote,
encrypted_note: TransmittedNoteCiphertext,
cv_net: ValueCommitment,
authorization: T,
) -> Self {
@ -70,7 +70,7 @@ impl<T> Action<T> {
}
/// Returns the encrypted note ciphertext.
pub fn encrypted_note(&self) -> &EncryptedNote {
pub fn encrypted_note(&self) -> &TransmittedNoteCiphertext {
&self.encrypted_note
}
@ -174,7 +174,7 @@ pub trait Authorization {
/// A bundle of actions to be applied to the ledger.
#[derive(Debug)]
pub struct Bundle<T: Authorization> {
pub struct Bundle<T: Authorization, V> {
/// The list of actions that make up this bundle.
actions: NonEmpty<Action<T::SpendAuth>>,
/// Orchard-specific transaction-level flags for this bundle.
@ -182,19 +182,19 @@ pub struct Bundle<T: Authorization> {
/// The net value moved out of the Orchard shielded pool.
///
/// This is the sum of Orchard spends minus the sum of Orchard outputs.
value_balance: ValueSum,
value_balance: V,
/// The root of the Orchard commitment tree that this bundle commits to.
anchor: Anchor,
/// The authorization for this bundle.
authorization: T,
}
impl<T: Authorization> Bundle<T> {
impl<T: Authorization, V> Bundle<T, V> {
/// Constructs a `Bundle` from its constituent parts.
pub fn from_parts(
actions: NonEmpty<Action<T::SpendAuth>>,
flags: Flags,
value_balance: ValueSum,
value_balance: V,
anchor: Anchor,
authorization: T,
) -> Self {
@ -220,7 +220,7 @@ impl<T: Authorization> Bundle<T> {
/// Returns the net value moved into or out of the Orchard shielded pool.
///
/// This is the sum of Orchard spends minus the sum Orchard outputs.
pub fn value_balance(&self) -> &ValueSum {
pub fn value_balance(&self) -> &V {
&self.value_balance
}
@ -242,13 +242,28 @@ impl<T: Authorization> Bundle<T> {
todo!()
}
/// Construct a new bundle by applying a transformation that might fail
/// to the value balance.
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,
})
}
/// Transitions this bundle from one authorization state to another.
pub fn authorize<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> {
) -> Bundle<U, V> {
let authorization = self.authorization;
Bundle {
actions: self
@ -267,7 +282,7 @@ impl<T: Authorization> Bundle<T> {
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>, E> {
) -> Result<Bundle<U, V>, E> {
let authorization = self.authorization;
let new_actions = self
.actions
@ -304,9 +319,19 @@ impl Authorized {
binding_signature,
}
}
/// Return the proof component of the authorizing data.
pub fn proof(&self) -> &Proof {
&self.proof
}
/// Return the binding signature.
pub fn binding_signature(&self) -> &redpallas::Signature<Binding> {
&self.binding_signature
}
}
impl Bundle<Authorized> {
impl<V> Bundle<Authorized, V> {
/// Computes a commitment to the authorizing data within for this bundle.
///
/// This together with `Bundle::commitment` bind the entire bundle.
@ -325,3 +350,224 @@ pub struct BundleCommitment;
/// A commitment to the authorizing data within a bundle of actions.
#[derive(Debug)]
pub struct BundleAuthorizingCommitment;
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use nonempty::NonEmpty;
use rand_7::{rngs::StdRng, SeedableRng};
use reddsa::orchard::SpendAuth;
use proptest::collection::vec;
use proptest::prelude::*;
use crate::{
circuit::Proof,
note::{
commitment::ExtractedNoteCommitment, nullifier::testing::arb_nullifier,
testing::arb_note, TransmittedNoteCiphertext,
},
primitives::redpallas::{
self,
testing::{
arb_binding_signing_key, arb_spendauth_signing_key, arb_spendauth_verification_key,
},
},
value::{
testing::arb_note_value_bounded, NoteValue, ValueCommitTrapdoor, ValueCommitment,
ValueSum, MAX_NOTE_VALUE,
},
Anchor,
};
use super::{Action, Authorization, Authorized, Bundle, Flags};
/// Marker for an unauthorized bundle with no proofs or signatures.
#[derive(Debug)]
pub struct Unauthorized;
impl Authorization for Unauthorized {
type SpendAuth = ();
}
prop_compose! {
/// Generate an action without authorization data.
pub fn arb_unauthorized_action(spend_value: NoteValue, output_value: NoteValue)(
nf in arb_nullifier(),
rk in arb_spendauth_verification_key(),
note in arb_note(output_value),
) -> Action<()> {
let cmx = ExtractedNoteCommitment::from(note.commitment());
let cv_net = ValueCommitment::derive(
(spend_value - output_value).unwrap(),
ValueCommitTrapdoor::zero()
);
// FIXME: make a real one from the note.
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
out_ciphertext: [0u8; 80]
};
Action {
nf,
rk,
cmx,
encrypted_note,
cv_net,
authorization: ()
}
}
}
/// Generate an unauthorized action having spend and output values less than MAX_NOTE_VALUE / n_actions.
pub fn arb_unauthorized_action_n(
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(spend_value, output_value)
.prop_map(move |a| ((spend_value - output_value).unwrap(), a))
})
})
}
prop_compose! {
/// Generate an action with invalid (random) authorization data.
pub fn arb_action(spend_value: NoteValue, output_value: NoteValue)(
nf in arb_nullifier(),
sk in arb_spendauth_signing_key(),
note in arb_note(output_value),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY),
) -> Action<redpallas::Signature<SpendAuth>> {
let cmx = ExtractedNoteCommitment::from(note.commitment());
let cv_net = ValueCommitment::derive(
(spend_value - output_value).unwrap(),
ValueCommitTrapdoor::zero()
);
// FIXME: make a real one from the note.
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
out_ciphertext: [0u8; 80]
};
let rng = StdRng::from_seed(rng_seed);
Action {
nf,
rk: redpallas::VerificationKey::from(&sk),
cmx,
encrypted_note,
cv_net,
authorization: sk.sign(rng, &fake_sighash),
}
}
}
/// Generate an authorized action having spend and output values less than MAX_NOTE_VALUE / n_actions.
pub fn arb_action_n(
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(spend_value, output_value)
.prop_map(move |a| ((spend_value - output_value).unwrap(), a))
})
})
}
prop_compose! {
/// Create an arbitrary set of flags.
pub fn arb_flags()(spends_enabled in prop::bool::ANY, outputs_enabled in prop::bool::ANY) -> Flags {
Flags::from_parts(spends_enabled, outputs_enabled)
}
}
prop_compose! {
/// Generate an arbitrary unauthorized bundle. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_bundle`]
pub fn arb_unauthorized_bundle()
(
n_actions in 1usize..100,
flags in arb_flags(),
)
(
acts in vec(arb_unauthorized_action_n(n_actions, flags), n_actions),
anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor),
flags in Just(flags)
) -> Bundle<Unauthorized, ValueSum> {
let (balances, actions): (Vec<ValueSum>, Vec<Action<_>>) = acts.into_iter().unzip();
Bundle::from_parts(
NonEmpty::from_vec(actions).unwrap(),
flags,
balances.into_iter().sum::<Result<ValueSum, _>>().unwrap(),
anchor,
Unauthorized
)
}
}
prop_compose! {
/// Generate an arbitrary bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_bundle`]
pub fn arb_bundle()
(
n_actions in 1usize..100,
flags in arb_flags(),
)
(
acts in vec(arb_action_n(n_actions, flags), n_actions),
anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor),
sk in arb_binding_signing_key(),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_proof in vec(prop::num::u8::ANY, 1973),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY),
flags in Just(flags)
) -> Bundle<Authorized, ValueSum> {
let (balances, actions): (Vec<ValueSum>, Vec<Action<_>>) = acts.into_iter().unzip();
let rng = StdRng::from_seed(rng_seed);
Bundle::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),
}
)
}
}
}

View File

@ -7,11 +7,10 @@ use halo2::{
use pasta_curves::{pallas, vesta};
use crate::{
note::ExtractedNoteCommitment,
note::{nullifier::Nullifier, ExtractedNoteCommitment},
primitives::redpallas::{SpendAuth, VerificationKey},
tree::Anchor,
value::ValueCommitment,
Nullifier,
};
pub(crate) mod gadget;
@ -120,6 +119,12 @@ impl Instance {
#[derive(Debug)]
pub struct Proof(Vec<u8>);
impl AsRef<[u8]> for Proof {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Proof {
/// Creates a proof for the given circuits and instances.
pub fn create(
@ -156,6 +161,11 @@ impl Proof {
Err(plonk::Error::ConstraintSystemFailure)
}
}
/// Constructs a new Proof value.
pub fn new(bytes: Vec<u8>) -> Self {
Proof(bytes)
}
}
#[cfg(test)]
@ -174,6 +184,7 @@ mod tests {
value::{ValueCommitTrapdoor, ValueCommitment},
};
// TODO: recast as a proptest
#[test]
fn round_trip() {
let mut rng = OsRng;
@ -195,7 +206,7 @@ mod tests {
(
Circuit {},
Instance {
anchor: Anchor,
anchor: Anchor([0; 32]),
cv_net,
nf_old,
rk,

View File

@ -25,7 +25,7 @@ use crate::{
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SpendingKey([u8; 32]);
impl SpendingKey {
@ -109,7 +109,7 @@ impl From<&SpendingKey> for SpendAuthorizingKey {
/// $\mathsf{ak}$ but stored here as a RedPallas verification key.
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SpendValidatingKey(redpallas::VerificationKey<SpendAuth>);
impl From<&SpendAuthorizingKey> for SpendValidatingKey {
@ -138,7 +138,7 @@ impl SpendValidatingKey {
/// [`Nullifier`]: crate::note::Nullifier
/// [`Note`]: crate::note::Note
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct NullifierDerivingKey(pallas::Base);
impl From<&SpendingKey> for NullifierDerivingKey {
@ -158,7 +158,7 @@ impl NullifierDerivingKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
struct CommitIvkRandomness(pallas::Scalar);
impl From<&SpendingKey> for CommitIvkRandomness {
@ -175,7 +175,7 @@ impl From<&SpendingKey> for CommitIvkRandomness {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct FullViewingKey {
ak: SpendValidatingKey,
nk: NullifierDerivingKey,
@ -287,7 +287,7 @@ impl DiversifierKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Diversifier([u8; 11]);
impl Diversifier {
@ -343,7 +343,7 @@ impl IncomingViewingKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct OutgoingViewingKey([u8; 32]);
impl From<&FullViewingKey> for OutgoingViewingKey {
@ -357,7 +357,7 @@ impl From<&FullViewingKey> for OutgoingViewingKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct DiversifiedTransmissionKey(pallas::Point);
impl DiversifiedTransmissionKey {
@ -374,3 +374,25 @@ impl DiversifiedTransmissionKey {
self.0.to_bytes()
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use super::SpendingKey;
prop_compose! {
/// Generate a uniformly distributed fake note commitment value.
pub fn arb_spending_key()(
key in prop::array::uniform32(prop::num::u8::ANY)
.prop_map(SpendingKey::from_bytes)
.prop_filter(
"Values must correspond to valid Orchard spending keys.",
|opt| bool::from(opt.is_some())
)
) -> SpendingKey {
key.unwrap()
}
}
}

View File

@ -17,16 +17,19 @@
#![deny(unsafe_code)]
mod address;
mod builder;
pub mod builder;
pub mod bundle;
mod circuit;
mod constants;
pub mod keys;
mod note;
pub mod note;
pub mod primitives;
mod spec;
mod tree;
pub mod value;
pub use address::Address;
pub use note::{EncryptedNote, Note, NoteCommitment, Nullifier};
pub use bundle::Bundle;
pub use circuit::Proof;
pub use note::Note;
pub use tree::Anchor;

View File

@ -1,3 +1,4 @@
//! Data structures used for note construction.
use group::GroupEncoding;
use pasta_curves::pallas;
use rand::RngCore;
@ -9,10 +10,10 @@ use crate::{
Address,
};
mod commitment;
pub(crate) mod commitment;
pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment};
mod nullifier;
pub(crate) mod nullifier;
pub use self::nullifier::Nullifier;
/// The ZIP 212 seed randomness for a note.
@ -135,4 +136,47 @@ impl Note {
/// An encrypted note.
#[derive(Debug)]
pub struct EncryptedNote;
pub struct TransmittedNoteCiphertext {
/// The serialization of the ephemeral public key
pub epk_bytes: [u8; 32],
/// The encrypted note ciphertext
pub enc_ciphertext: [u8; 580],
/// An encrypted value that allows the holder of the outgoing cipher
/// key for the note to recover the note plaintext.
pub out_ciphertext: [u8; 80],
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use crate::{
address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue,
};
use super::{Note, RandomSeed};
prop_compose! {
/// Generate an arbitrary random seed
pub(crate) fn arb_rseed()(elems in prop::array::uniform32(prop::num::u8::ANY)) -> RandomSeed {
RandomSeed(elems)
}
}
prop_compose! {
/// Generate an action without authorization data.
pub fn arb_note(value: NoteValue)(
recipient in arb_address(),
rho in arb_nullifier(),
rseed in arb_rseed(),
) -> Note {
Note {
recipient,
value,
rho,
rseed,
}
}
}
}

View File

@ -2,7 +2,7 @@ use std::iter;
use bitvec::{array::BitArray, order::Lsb0};
use ff::PrimeField;
use pasta_curves::pallas;
use pasta_curves::{arithmetic::FieldExt, pallas};
use subtle::CtOption;
use crate::{
@ -59,6 +59,18 @@ impl NoteCommitment {
#[derive(Clone, Debug)]
pub struct ExtractedNoteCommitment(pub(super) pallas::Base);
impl ExtractedNoteCommitment {
/// Deserialize the extracted note commitment from a byte array.
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Base::from_bytes(bytes).map(ExtractedNoteCommitment)
}
/// Serialize the value commitment to its canonical byte representation.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
}
impl From<NoteCommitment> for ExtractedNoteCommitment {
fn from(cm: NoteCommitment) -> Self {
ExtractedNoteCommitment(extract_p(&cm.0))

View File

@ -1,7 +1,8 @@
use group::Group;
use halo2::arithmetic::CurveExt;
use pasta_curves::pallas;
use pasta_curves::{arithmetic::FieldExt, pallas};
use rand::RngCore;
use subtle::CtOption;
use super::NoteCommitment;
use crate::{
@ -11,7 +12,7 @@ use crate::{
/// A unique nullifier for a note.
#[derive(Clone, Debug)]
pub struct Nullifier(pub(super) pallas::Base);
pub struct Nullifier(pub(crate) pallas::Base);
impl Nullifier {
/// Generates a dummy nullifier for use as $\rho$ in dummy spent notes.
@ -30,6 +31,16 @@ impl Nullifier {
Nullifier(extract_p(&pallas::Point::random(rng)))
}
/// Deserialize the nullifier from a byte array.
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Base::from_bytes(bytes).map(Nullifier)
}
/// Serialize the nullifier to its canonical byte representation.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
/// $DeriveNullifier$.
///
/// Defined in [Zcash Protocol Spec § 4.16: Note Commitments and Nullifiers][commitmentsandnullifiers].
@ -46,3 +57,27 @@ impl Nullifier {
Nullifier(extract_p(&(k * mod_r_p(nk.prf_nf(rho) + psi) + cm.0)))
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use group::GroupEncoding;
use pasta_curves::pallas;
use super::Nullifier;
use crate::spec::extract_p;
prop_compose! {
/// Generate a uniformly distributed nullifier value.
pub fn arb_nullifier()(
coord in prop::array::uniform32(any::<u8>()).prop_map(|b| pallas::Point::from_bytes(&b)).prop_filter(
"Must generate a valid Pallas point",
|p| p.is_some().into()
)
) -> Nullifier {
Nullifier(extract_p(&coord.unwrap()))
}
}
}

View File

@ -96,6 +96,18 @@ impl VerificationKey<SpendAuth> {
#[derive(Debug)]
pub struct Signature<T: SigType>(reddsa::Signature<T>);
impl<T: SigType> From<[u8; 64]> for Signature<T> {
fn from(bytes: [u8; 64]) -> Self {
Signature(bytes.into())
}
}
impl<T: SigType> From<&Signature<T>> for [u8; 64] {
fn from(sig: &Signature<T>) -> Self {
sig.0.into()
}
}
pub(crate) mod private {
use super::{Binding, SpendAuth};
@ -105,3 +117,49 @@ pub(crate) mod private {
impl Sealed for Binding {}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use std::convert::TryFrom;
use proptest::prelude::*;
use super::{Binding, SigningKey, SpendAuth, VerificationKey};
prop_compose! {
/// Generate a uniformly distributed RedDSA spend authorization signing key.
pub fn arb_spendauth_signing_key()(
sk in prop::array::uniform32(prop::num::u8::ANY)
.prop_map(reddsa::SigningKey::try_from)
.prop_filter("Values must be parseable as valid signing keys", |r| r.is_ok())
) -> SigningKey<SpendAuth> {
SigningKey(sk.unwrap())
}
}
prop_compose! {
/// Generate a uniformly distributed RedDSA binding signing key.
pub fn arb_binding_signing_key()(
sk in prop::array::uniform32(prop::num::u8::ANY)
.prop_map(reddsa::SigningKey::try_from)
.prop_filter("Values must be parseable as valid signing keys", |r| r.is_ok())
) -> SigningKey<Binding> {
SigningKey(sk.unwrap())
}
}
prop_compose! {
/// Generate a uniformly distributed RedDSA spend authorization verification key.
pub fn arb_spendauth_verification_key()(sk in arb_spendauth_signing_key()) -> VerificationKey<SpendAuth> {
VerificationKey::from(&sk)
}
}
prop_compose! {
/// Generate a uniformly distributed RedDSA binding verification key.
pub fn arb_binding_verification_key()(sk in arb_binding_signing_key()) -> VerificationKey<Binding> {
VerificationKey::from(&sk)
}
}
}

View File

@ -2,7 +2,7 @@ use rand::RngCore;
/// The root of an Orchard commitment tree.
#[derive(Clone, Debug)]
pub struct Anchor;
pub struct Anchor(pub [u8; 32]);
#[derive(Debug)]
pub struct MerklePath;
@ -10,7 +10,6 @@ pub struct MerklePath;
impl MerklePath {
/// Generates a dummy Merkle path for use in dummy spent notes.
pub(crate) fn dummy(_rng: &mut impl RngCore) -> Self {
// TODO
MerklePath
}
}

View File

@ -14,8 +14,8 @@
//! [`Action`]: crate::bundle::Action
//! [`Bundle`]: crate::bundle::Bundle
use std::convert::TryInto;
use std::fmt;
use std::convert::{TryFrom, TryInto};
use std::fmt::{self, Debug};
use std::iter::Sum;
use std::ops::{Add, Sub};
@ -27,9 +27,23 @@ use pasta_curves::{
pallas,
};
use rand::RngCore;
use subtle::CtOption;
use crate::primitives::redpallas::{self, Binding};
use std::ops::RangeInclusive;
/// Maximum note value.
pub const MAX_NOTE_VALUE: u64 = u64::MAX;
/// The valid range of the scalar multiplication used in ValueCommit^Orchard.
///
/// Defined in a note in [Zcash Protocol Spec § 4.17.4: Action Statement (Orchard)][actionstatement].
///
/// [actionstatement]: https://zips.z.cash/protocol/nu5.pdf#actionstatement
pub const VALUE_SUM_RANGE: RangeInclusive<i128> =
-(MAX_NOTE_VALUE as i128)..=MAX_NOTE_VALUE as i128;
/// A value operation overflowed.
#[derive(Debug)]
pub struct OverflowError;
@ -66,18 +80,21 @@ impl NoteValue {
}
impl Sub for NoteValue {
type Output = Result<ValueSum, OverflowError>;
type Output = Option<ValueSum>;
#[allow(clippy::suspicious_arithmetic_impl)]
fn sub(self, rhs: Self) -> Self::Output {
let a: i64 = self.0.try_into().map_err(|_| OverflowError)?;
let b: i64 = rhs.0.try_into().map_err(|_| OverflowError)?;
Ok(ValueSum(a - b))
let a = self.0 as i128;
let b = rhs.0 as i128;
a.checked_sub(b)
.filter(|v| VALUE_SUM_RANGE.contains(v))
.map(ValueSum)
}
}
/// A sum of Orchard note values.
/// A sum of Orchard note values
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ValueSum(i64);
pub struct ValueSum(i128);
impl ValueSum {
pub(crate) fn zero() -> Self {
@ -90,21 +107,39 @@ impl ValueSum {
/// This only enforces that the value is a signed 63-bit integer. Callers should
/// enforce any additional constraints on the value's valid range themselves.
pub fn from_raw(value: i64) -> Self {
ValueSum(value)
ValueSum(value as i128)
}
}
impl Add for ValueSum {
type Output = Result<ValueSum, OverflowError>;
type Output = Option<ValueSum>;
#[allow(clippy::suspicious_arithmetic_impl)]
fn add(self, rhs: Self) -> Self::Output {
self.0.checked_add(rhs.0).map(ValueSum).ok_or(OverflowError)
self.0
.checked_add(rhs.0)
.filter(|v| VALUE_SUM_RANGE.contains(v))
.map(ValueSum)
}
}
impl<'a> Sum<&'a ValueSum> for Result<ValueSum, OverflowError> {
fn sum<I: Iterator<Item = &'a ValueSum>>(iter: I) -> Self {
iter.fold(Ok(ValueSum(0)), |acc, cv| acc? + *cv)
iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + *v).ok_or(OverflowError))
}
}
impl Sum<ValueSum> for Result<ValueSum, OverflowError> {
fn sum<I: Iterator<Item = ValueSum>>(iter: I) -> Self {
iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + v).ok_or(OverflowError))
}
}
impl TryFrom<ValueSum> for i64 {
type Error = OverflowError;
fn try_from(v: ValueSum) -> Result<i64, Self::Error> {
i64::try_from(v.0).map_err(|_| OverflowError)
}
}
@ -190,11 +225,12 @@ impl ValueCommitment {
let hasher = pallas::Point::hash_to_curve("z.cash:Orchard-cv");
let V = hasher(b"v");
let R = hasher(b"r");
let abs_value = u64::try_from(value.0.abs()).expect("value must be in valid range");
let value = if value.0.is_negative() {
-pallas::Scalar::from_u64((-value.0) as u64)
-pallas::Scalar::from_u64(abs_value)
} else {
pallas::Scalar::from_u64(value.0 as u64)
pallas::Scalar::from_u64(abs_value)
};
ValueCommitment(V * value + R * rcv.0)
@ -204,22 +240,29 @@ impl ValueCommitment {
// TODO: impl From<pallas::Point> for redpallas::VerificationKey.
self.0.to_bytes().try_into().unwrap()
}
/// Deserialize a value commitment from its byte representation
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<ValueCommitment> {
pallas::Point::from_bytes(bytes).map(ValueCommitment)
}
/// Serialize this value commitment to its canonical byte representation.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
}
#[cfg(test)]
mod tests {
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use pasta_curves::{arithmetic::FieldExt, pallas};
use proptest::prelude::*;
use super::{OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum};
use crate::primitives::redpallas;
/// Zcash's maximum money amount. Used as a bound in proptests so we don't artifically
/// overflow `ValueSum`'s size.
const MAX_MONEY: i64 = 21_000_000 * 1_0000_0000;
use super::{NoteValue, ValueCommitTrapdoor, ValueSum, MAX_NOTE_VALUE, VALUE_SUM_RANGE};
prop_compose! {
fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar {
/// Generate an arbitrary Pallas scalar.
pub fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar {
// Instead of rejecting out-of-range bytes, let's reduce them.
let mut buf = [0; 64];
buf[..32].copy_from_slice(&bytes);
@ -228,21 +271,68 @@ mod tests {
}
prop_compose! {
fn arb_value_sum(bound: i64)(value in -bound..bound) -> ValueSum {
ValueSum(value)
/// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values.
pub fn arb_value_sum()(value in VALUE_SUM_RANGE) -> ValueSum {
ValueSum(value as i128)
}
}
prop_compose! {
fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
/// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values.
pub fn arb_value_sum_bounded(bound: NoteValue)(value in -(bound.0 as i128)..=(bound.0 as i128)) -> ValueSum {
ValueSum(value as i128)
}
}
prop_compose! {
/// Generate an arbitrary ValueCommitTrapdoor
pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
ValueCommitTrapdoor(rcv)
}
}
prop_compose! {
/// Generate an arbitrary value in the range of valid nonnegative Zcash amounts.
pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue {
NoteValue(value)
}
}
prop_compose! {
/// Generate an arbitrary value in the range of valid positive Zcash amounts
/// less than a specified value.
pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue {
NoteValue(value)
}
}
prop_compose! {
/// Generate an arbitrary value in the range of valid positive Zcash amounts
/// less than a specified value.
pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue {
NoteValue(value)
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::{
testing::{arb_note_value_bounded, arb_trapdoor, arb_value_sum_bounded},
OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum, MAX_NOTE_VALUE,
};
use crate::primitives::redpallas;
proptest! {
#[test]
fn bsk_consistent_with_bvk(
values in prop::collection::vec((arb_value_sum(MAX_MONEY), arb_trapdoor()), 1..10),
values in (1usize..10).prop_flat_map(|n_values|
arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound|
prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values)
)
)
) {
let value_balance = values
.iter()