Merge pull request #1023 from zcash/741-sapling-builder-refactor

Refactor Sapling builder to separate out proof generation
This commit is contained in:
str4d 2023-11-02 23:41:31 +00:00 committed by GitHub
commit 44c64e1fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 711 additions and 319 deletions

View File

@ -8,7 +8,7 @@ use zcash_primitives::{
sapling::{ sapling::{
self, self,
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::TxProver as SaplingProver, prover::{OutputProver, SpendProver},
Node, Node,
}, },
transaction::{ transaction::{
@ -191,7 +191,8 @@ where
pub fn create_spend_to_address<DbT, ParamsT>( pub fn create_spend_to_address<DbT, ParamsT>(
wallet_db: &mut DbT, wallet_db: &mut DbT,
params: &ParamsT, params: &ParamsT,
prover: impl SaplingProver, spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
to: &RecipientAddress, to: &RecipientAddress,
amount: NonNegativeAmount, amount: NonNegativeAmount,
@ -231,7 +232,15 @@ where
change_memo, change_memo,
)?; )?;
create_proposed_transaction(wallet_db, params, prover, usk, ovk_policy, &proposal) create_proposed_transaction(
wallet_db,
params,
spend_prover,
output_prover,
usk,
ovk_policy,
&proposal,
)
} }
/// Constructs a transaction that sends funds as specified by the `request` argument /// Constructs a transaction that sends funds as specified by the `request` argument
@ -289,7 +298,8 @@ where
pub fn spend<DbT, ParamsT, InputsT>( pub fn spend<DbT, ParamsT, InputsT>(
wallet_db: &mut DbT, wallet_db: &mut DbT,
params: &ParamsT, params: &ParamsT,
prover: impl SaplingProver, spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT, input_selector: &InputsT,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest, request: zip321::TransactionRequest,
@ -324,7 +334,15 @@ where
min_confirmations, min_confirmations,
)?; )?;
create_proposed_transaction(wallet_db, params, prover, usk, ovk_policy, &proposal) create_proposed_transaction(
wallet_db,
params,
spend_prover,
output_prover,
usk,
ovk_policy,
&proposal,
)
} }
/// Select transaction inputs, compute fees, and construct a proposal for a transaction /// Select transaction inputs, compute fees, and construct a proposal for a transaction
@ -475,7 +493,8 @@ where
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT>( pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT>(
wallet_db: &mut DbT, wallet_db: &mut DbT,
params: &ParamsT, params: &ParamsT,
prover: impl SaplingProver, spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, DbT::NoteRef>, proposal: &Proposal<FeeRuleT, DbT::NoteRef>,
@ -663,7 +682,8 @@ where
} }
// Build the transaction with the specified fee rule // Build the transaction with the specified fee rule
let (tx, sapling_build_meta) = builder.build(&prover, proposal.fee_rule())?; let (tx, sapling_build_meta) =
builder.build(spend_prover, output_prover, proposal.fee_rule())?;
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
let sapling_outputs = let sapling_outputs =
@ -768,7 +788,8 @@ where
pub fn shield_transparent_funds<DbT, ParamsT, InputsT>( pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
wallet_db: &mut DbT, wallet_db: &mut DbT,
params: &ParamsT, params: &ParamsT,
prover: impl SaplingProver, spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT, input_selector: &InputsT,
shielding_threshold: NonNegativeAmount, shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
@ -798,7 +819,15 @@ where
min_confirmations, min_confirmations,
)?; )?;
create_proposed_transaction(wallet_db, params, prover, usk, OvkPolicy::Sender, &proposal) create_proposed_transaction(
wallet_db,
params,
spend_prover,
output_prover,
usk,
OvkPolicy::Sender,
&proposal,
)
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]

View File

@ -448,10 +448,12 @@ impl<Cache> TestState<Cache> {
>, >,
> { > {
let params = self.network(); let params = self.network();
let prover = test_prover();
create_spend_to_address( create_spend_to_address(
&mut self.db_data, &mut self.db_data,
&params, &params,
test_prover(), &prover,
&prover,
usk, usk,
to, to,
amount, amount,
@ -484,10 +486,12 @@ impl<Cache> TestState<Cache> {
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>, InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
{ {
let params = self.network(); let params = self.network();
let prover = test_prover();
spend( spend(
&mut self.db_data, &mut self.db_data,
&params, &params,
test_prover(), &prover,
&prover,
input_selector, input_selector,
usk, usk,
request, request,
@ -614,10 +618,12 @@ impl<Cache> TestState<Cache> {
FeeRuleT: FeeRule, FeeRuleT: FeeRule,
{ {
let params = self.network(); let params = self.network();
let prover = test_prover();
create_proposed_transaction( create_proposed_transaction(
&mut self.db_data, &mut self.db_data,
&params, &params,
test_prover(), &prover,
&prover,
usk, usk,
ovk_policy, ovk_policy,
proposal, proposal,
@ -647,10 +653,12 @@ impl<Cache> TestState<Cache> {
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>, InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
{ {
let params = self.network(); let params = self.network();
let prover = test_prover();
shield_transparent_funds( shield_transparent_funds(
&mut self.db_data, &mut self.db_data,
&params, &params,
test_prover(), &prover,
&prover,
input_selector, input_selector,
shielding_threshold, shielding_threshold,
usk, usk,

View File

@ -451,8 +451,9 @@ pub(crate) mod tests {
legacy::TransparentAddress, legacy::TransparentAddress,
memo::{Memo, MemoBytes}, memo::{Memo, MemoBytes},
sapling::{ sapling::{
note_encryption::try_sapling_output_recovery, prover::TxProver, Node, Note, note_encryption::try_sapling_output_recovery,
PaymentAddress, prover::{OutputProver, SpendProver},
Node, Note, PaymentAddress,
}, },
transaction::{ transaction::{
components::{amount::NonNegativeAmount, Amount}, components::{amount::NonNegativeAmount, Amount},
@ -497,7 +498,7 @@ pub(crate) mod tests {
zcash_primitives::transaction::components::{OutPoint, TxOut}, zcash_primitives::transaction::components::{OutPoint, TxOut},
}; };
pub(crate) fn test_prover() -> impl TxProver { pub(crate) fn test_prover() -> impl SpendProver + OutputProver {
match LocalTxProver::with_default_location() { match LocalTxProver::with_default_location() {
Some(tx_prover) => tx_prover, Some(tx_prover) => tx_prover,
None => { None => {

View File

@ -839,7 +839,7 @@ mod tests {
.unwrap(); .unwrap();
let (tx_a, _) = builder_a let (tx_a, _) = builder_a
.txn_builder .txn_builder
.build_zfuture(&prover, &fee_rule) .build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_a = tx_a.tze_bundle().unwrap(); let tze_a = tx_a.tze_bundle().unwrap();
@ -857,7 +857,7 @@ mod tests {
.unwrap(); .unwrap();
let (tx_b, _) = builder_b let (tx_b, _) = builder_b
.txn_builder .txn_builder
.build_zfuture(&prover, &fee_rule) .build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_b = tx_b.tze_bundle().unwrap(); let tze_b = tx_b.tze_bundle().unwrap();
@ -882,7 +882,7 @@ mod tests {
let (tx_c, _) = builder_c let (tx_c, _) = builder_c
.txn_builder .txn_builder
.build_zfuture(&prover, &fee_rule) .build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_c = tx_c.tze_bundle().unwrap(); let tze_c = tx_c.tze_bundle().unwrap();

View File

@ -12,13 +12,11 @@ use zcash_primitives::{
try_sapling_compact_note_decryption, try_sapling_note_decryption, try_sapling_compact_note_decryption, try_sapling_note_decryption,
PreparedIncomingViewingKey, SaplingDomain, PreparedIncomingViewingKey, SaplingDomain,
}, },
prover::mock::MockTxProver, prover::mock::{MockOutputProver, MockSpendProver},
value::NoteValue, value::NoteValue,
Diversifier, SaplingIvk, Diversifier, SaplingIvk,
}, },
transaction::components::sapling::{ transaction::components::sapling::{builder::SaplingBuilder, CompactOutputDescription},
builder::SaplingBuilder, CompactOutputDescription, GrothProofBytes, OutputDescription,
},
}; };
#[cfg(unix)] #[cfg(unix)]
@ -32,7 +30,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
// Construct a Sapling output. // Construct a Sapling output.
let output: OutputDescription<GrothProofBytes> = { let output = {
let diversifier = Diversifier([0; 11]); let diversifier = Diversifier([0; 11]);
let pa = valid_ivk.to_payment_address(diversifier).unwrap(); let pa = valid_ivk.to_payment_address(diversifier).unwrap();
@ -46,8 +44,8 @@ fn bench_note_decryption(c: &mut Criterion) {
MemoBytes::empty(), MemoBytes::empty(),
) )
.unwrap(); .unwrap();
let bundle = builder let (bundle, _) = builder
.build(&MockTxProver, &mut (), &mut rng, height, None) .build::<MockSpendProver, MockOutputProver, _>(&mut rng, height)
.unwrap() .unwrap()
.unwrap(); .unwrap();
bundle.shielded_outputs()[0].clone() bundle.shielded_outputs()[0].clone()

View File

@ -36,11 +36,11 @@ pub fn spend_sig<R: RngCore + CryptoRng>(
sighash: &[u8; 32], sighash: &[u8; 32],
rng: &mut R, rng: &mut R,
) -> Signature { ) -> Signature {
spend_sig_internal(ask, ar, sighash, rng) spend_sig_internal(&ask, ar, sighash, rng)
} }
pub(crate) fn spend_sig_internal<R: RngCore>( pub(crate) fn spend_sig_internal<R: RngCore>(
ask: PrivateKey, ask: &PrivateKey,
ar: jubjub::Fr, ar: jubjub::Fr,
sighash: &[u8; 32], sighash: &[u8; 32],
rng: &mut R, rng: &mut R,
@ -60,6 +60,29 @@ pub(crate) fn spend_sig_internal<R: RngCore>(
rsk.sign(&data_to_be_signed, rng, SPENDING_KEY_GENERATOR) rsk.sign(&data_to_be_signed, rng, SPENDING_KEY_GENERATOR)
} }
/// Verifies a spendAuthSig.
///
/// This only exists because the RedJubjub implementation inside `zcash_primitives` does
/// not implement key prefixing (which was added in response to a Sapling audit). This
/// will be fixed by migrating to the redjubjub crate.
pub(crate) fn verify_spend_sig(
ak: &PublicKey,
alpha: jubjub::Fr,
sighash: &[u8; 32],
sig: &Signature,
) -> bool {
// We compute `rk` (needed for key prefixing)
let rk = ak.randomize(alpha, SPENDING_KEY_GENERATOR);
// Compute the signature's message for rk/spend_auth_sig
let mut data_to_be_signed = [0u8; 64];
data_to_be_signed[0..32].copy_from_slice(&rk.0.to_bytes());
data_to_be_signed[32..64].copy_from_slice(&sighash[..]);
// Do the verifying
rk.verify(&data_to_be_signed, sig, SPENDING_KEY_GENERATOR)
}
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
pub use super::{ pub use super::{

View File

@ -38,7 +38,7 @@
//! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html //! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html
use bitvec::{array::BitArray, order::Lsb0}; use bitvec::{array::BitArray, order::Lsb0};
use ff::Field; use ff::{Field, PrimeField};
use group::GroupEncoding; use group::GroupEncoding;
use rand::RngCore; use rand::RngCore;
use subtle::CtOption; use subtle::CtOption;
@ -86,6 +86,21 @@ impl ValueCommitTrapdoor {
ValueCommitTrapdoor(jubjub::Scalar::random(rng)) ValueCommitTrapdoor(jubjub::Scalar::random(rng))
} }
/// Constructs `ValueCommitTrapdoor` from the byte representation of a scalar.
///
/// Returns a `None` [`CtOption`] if `bytes` is not a canonical representation of a
/// Jubjub scalar.
///
/// This is a low-level API, requiring a detailed understanding of the
/// [use of value commitment trapdoors][saplingbalance] in the Zcash protocol
/// to use correctly and securely. It is intended to be used in combination
/// with [`ValueCommitment::derive`].
///
/// [saplingbalance]: https://zips.z.cash/protocol/protocol.pdf#saplingbalance
pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
jubjub::Scalar::from_repr(bytes).map(ValueCommitTrapdoor)
}
/// Returns the inner Jubjub scalar representing this trapdoor. /// Returns the inner Jubjub scalar representing this trapdoor.
/// ///
/// This is public for access by `zcash_proofs`. /// This is public for access by `zcash_proofs`.

View File

@ -123,6 +123,14 @@ impl Sub<&ValueCommitTrapdoor> for ValueCommitTrapdoor {
} }
} }
impl Sub<TrapdoorSum> for TrapdoorSum {
type Output = TrapdoorSum;
fn sub(self, rhs: Self) -> Self::Output {
TrapdoorSum(self.0 - rhs.0)
}
}
impl SubAssign<&ValueCommitTrapdoor> for TrapdoorSum { impl SubAssign<&ValueCommitTrapdoor> for TrapdoorSum {
fn sub_assign(&mut self, rhs: &ValueCommitTrapdoor) { fn sub_assign(&mut self, rhs: &ValueCommitTrapdoor) {
self.0 -= rhs.0; self.0 -= rhs.0;
@ -207,6 +215,14 @@ impl SubAssign<&ValueCommitment> for CommitmentSum {
} }
} }
impl Sub<CommitmentSum> for CommitmentSum {
type Output = CommitmentSum;
fn sub(self, rhs: Self) -> Self::Output {
CommitmentSum(self.0 - rhs.0)
}
}
impl Sum<ValueCommitment> for CommitmentSum { impl Sum<ValueCommitment> for CommitmentSum {
fn sum<I: Iterator<Item = ValueCommitment>>(iter: I) -> Self { fn sum<I: Iterator<Item = ValueCommitment>>(iter: I) -> Self {
iter.fold(CommitmentSum::zero(), |acc, cv| acc + &cv) iter.fold(CommitmentSum::zero(), |acc, cv| acc + &cv)

View File

@ -12,7 +12,11 @@ use crate::{
keys::OutgoingViewingKey, keys::OutgoingViewingKey,
legacy::TransparentAddress, legacy::TransparentAddress,
memo::MemoBytes, memo::MemoBytes,
sapling::{self, prover::TxProver, Diversifier, Note, PaymentAddress}, sapling::{
self,
prover::{OutputProver, SpendProver},
redjubjub, Diversifier, Note, PaymentAddress,
},
transaction::{ transaction::{
components::{ components::{
amount::{Amount, BalanceError}, amount::{Amount, BalanceError},
@ -162,6 +166,7 @@ pub struct Builder<'a, P, R> {
// `add_sapling_spend` or `add_orchard_spend`, we will build an unauthorized, unproven // `add_sapling_spend` or `add_orchard_spend`, we will build an unauthorized, unproven
// transaction, and then the caller will be responsible for using the spending keys or their // transaction, and then the caller will be responsible for using the spending keys or their
// derivatives for proving and signing to complete transaction creation. // derivatives for proving and signing to complete transaction creation.
sapling_asks: Vec<redjubjub::PrivateKey>,
orchard_saks: Vec<orchard::keys::SpendAuthorizingKey>, orchard_saks: Vec<orchard::keys::SpendAuthorizingKey>,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder<'a, TransactionData<Unauthorized>>, tze_builder: TzeBuilder<'a, TransactionData<Unauthorized>>,
@ -266,6 +271,7 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(params, target_height), sapling_builder: SaplingBuilder::new(params, target_height),
orchard_builder, orchard_builder,
sapling_asks: vec![],
orchard_saks: Vec::new(), orchard_saks: Vec::new(),
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(), tze_builder: TzeBuilder::empty(),
@ -329,7 +335,12 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
merkle_path: sapling::MerklePath, merkle_path: sapling::MerklePath,
) -> Result<(), sapling_builder::Error> { ) -> Result<(), sapling_builder::Error> {
self.sapling_builder self.sapling_builder
.add_spend(&mut self.rng, extsk, diversifier, note, merkle_path) .add_spend(&mut self.rng, &extsk, diversifier, note, merkle_path)?;
self.sapling_asks
.push(redjubjub::PrivateKey(extsk.expsk.ask));
Ok(())
} }
/// Adds a Sapling address to send funds to. /// Adds a Sapling address to send funds to.
@ -432,13 +443,14 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
/// ///
/// Upon success, returns a tuple containing the final transaction, and the /// Upon success, returns a tuple containing the final transaction, and the
/// [`SaplingMetadata`] generated during the build process. /// [`SaplingMetadata`] generated during the build process.
pub fn build<FR: FeeRule>( pub fn build<SP: SpendProver, OP: OutputProver, FR: FeeRule>(
self, self,
prover: &impl TxProver, spend_prover: &SP,
output_prover: &OP,
fee_rule: &FR, fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> { ) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
let fee = self.get_fee(fee_rule).map_err(Error::Fee)?; let fee = self.get_fee(fee_rule).map_err(Error::Fee)?;
self.build_internal(prover, fee.into()) self.build_internal(spend_prover, output_prover, fee.into())
} }
/// Builds a transaction from the configured spends and outputs. /// Builds a transaction from the configured spends and outputs.
@ -446,9 +458,10 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
/// Upon success, returns a tuple containing the final transaction, and the /// Upon success, returns a tuple containing the final transaction, and the
/// [`SaplingMetadata`] generated during the build process. /// [`SaplingMetadata`] generated during the build process.
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
pub fn build_zfuture<FR: FutureFeeRule>( pub fn build_zfuture<SP: SpendProver, OP: OutputProver, FR: FutureFeeRule>(
self, self,
prover: &impl TxProver, spend_prover: &SP,
output_prover: &OP,
fee_rule: &FR, fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> { ) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
let fee = fee_rule let fee = fee_rule
@ -464,12 +477,13 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
) )
.map_err(Error::Fee)?; .map_err(Error::Fee)?;
self.build_internal(prover, fee.into()) self.build_internal(spend_prover, output_prover, fee.into())
} }
fn build_internal<FE>( fn build_internal<SP: SpendProver, OP: OutputProver, FE>(
self, self,
prover: &impl TxProver, spend_prover: &SP,
output_prover: &OP,
fee: Amount, fee: Amount,
) -> Result<(Transaction, SaplingMetadata), Error<FE>> { ) -> Result<(Transaction, SaplingMetadata), Error<FE>> {
let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); let consensus_branch_id = BranchId::for_height(&self.params, self.target_height);
@ -497,17 +511,27 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
let transparent_bundle = self.transparent_builder.build(); let transparent_bundle = self.transparent_builder.build();
let mut rng = self.rng; let mut rng = self.rng;
let mut ctx = prover.new_sapling_proving_context(); let (sapling_bundle, tx_metadata) = match self
let sapling_bundle = self
.sapling_builder .sapling_builder
.build( .build::<SP, OP, _>(&mut rng, self.target_height)
prover, .map_err(Error::SaplingBuild)?
&mut ctx, .map(|(bundle, tx_metadata)| {
&mut rng, // We need to create proofs before signatures, because we still support
self.target_height, // creating V4 transactions, which commit to the Sapling proofs in the
self.progress_notifier.as_ref(), // transaction digest.
) (
.map_err(Error::SaplingBuild)?; bundle.create_proofs(
spend_prover,
output_prover,
&mut rng,
self.progress_notifier.as_ref(),
),
tx_metadata,
)
}) {
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()),
};
let orchard_bundle: Option<orchard::Bundle<_, Amount>> = let orchard_bundle: Option<orchard::Bundle<_, Amount>> =
if let Some(builder) = self.orchard_builder { if let Some(builder) = self.orchard_builder {
@ -560,17 +584,17 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
let shielded_sig_commitment = let shielded_sig_commitment =
signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts); signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
let (sapling_bundle, tx_metadata) = match unauthed_tx let sapling_bundle = unauthed_tx
.sapling_bundle .sapling_bundle
.map(|b| { .map(|b| {
b.apply_signatures(prover, &mut ctx, &mut rng, shielded_sig_commitment.as_ref()) b.apply_signatures(
&mut rng,
*shielded_sig_commitment.as_ref(),
&self.sapling_asks,
)
}) })
.transpose() .transpose()
.map_err(Error::SaplingBuild)? .map_err(Error::SaplingBuild)?;
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()),
};
let orchard_bundle = unauthed_tx let orchard_bundle = unauthed_tx
.orchard_bundle .orchard_bundle
@ -648,7 +672,7 @@ mod testing {
use super::{Builder, Error, SaplingMetadata}; use super::{Builder, Error, SaplingMetadata};
use crate::{ use crate::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
sapling::prover::mock::MockTxProver, sapling::prover::mock::{MockOutputProver, MockSpendProver},
transaction::fees::fixed, transaction::fees::fixed,
transaction::Transaction, transaction::Transaction,
}; };
@ -693,7 +717,11 @@ mod testing {
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> { pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> {
#[allow(deprecated)] #[allow(deprecated)]
self.build(&MockTxProver, &fixed::FeeRule::standard()) self.build(
&MockSpendProver,
&MockOutputProver,
&fixed::FeeRule::standard(),
)
} }
} }
} }
@ -710,10 +738,7 @@ mod tests {
legacy::TransparentAddress, legacy::TransparentAddress,
memo::MemoBytes, memo::MemoBytes,
sapling::{self, Node, Rseed}, sapling::{self, Node, Rseed},
transaction::components::{ transaction::components::amount::{Amount, BalanceError, NonNegativeAmount},
amount::{Amount, NonNegativeAmount},
sapling::builder::{self as sapling_builder},
},
zip32::ExtendedSpendingKey, zip32::ExtendedSpendingKey,
}; };
@ -759,6 +784,7 @@ mod tests {
tze_builder: std::marker::PhantomData, tze_builder: std::marker::PhantomData,
progress_notifier: None, progress_notifier: None,
orchard_builder: None, orchard_builder: None,
sapling_asks: vec![],
orchard_saks: Vec::new(), orchard_saks: Vec::new(),
}; };
@ -828,12 +854,9 @@ mod tests {
) )
.unwrap(); .unwrap();
// Expect a binding signature error, because our inputs aren't valid, but this shows // A binding signature (and bundle) is present because there is a Sapling spend.
// that a binding signature was attempted let (tx, _) = builder.mock_build().unwrap();
assert_matches!( assert!(tx.sapling_bundle().is_some());
builder.mock_build(),
Err(Error::SaplingBuild(sapling_builder::Error::BindingSig))
);
} }
#[test] #[test]
@ -950,9 +973,6 @@ mod tests {
// Succeeds if there is sufficient input // Succeeds if there is sufficient input
// 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in
//
// (Still fails because we are using a MockTxProver which doesn't correctly
// compute bindingSig.)
{ {
let mut builder = Builder::new(TEST_NETWORK, tx_height, None); let mut builder = Builder::new(TEST_NETWORK, tx_height, None);
builder builder
@ -982,8 +1002,8 @@ mod tests {
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
builder.mock_build(), builder.mock_build(),
Err(Error::SaplingBuild(sapling_builder::Error::BindingSig)) Ok((tx, _)) if tx.fee_paid(|_| Err(BalanceError::Overflow)).unwrap() == Amount::const_from_i64(10_000)
) );
} }
} }
} }

View File

@ -34,19 +34,11 @@ pub trait Authorization: Debug {
type AuthSig: Clone + Debug; type AuthSig: Clone + Debug;
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Unproven;
impl Authorization for Unproven {
type SpendProof = ();
type OutputProof = ();
type AuthSig = ();
}
/// Authorizing data for a bundle of Sapling spends and outputs, ready to be committed to /// Authorizing data for a bundle of Sapling spends and outputs, ready to be committed to
/// the ledger. /// the ledger.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Authorized { pub struct Authorized {
// TODO: Make this private.
pub binding_sig: redjubjub::Signature, pub binding_sig: redjubjub::Signature,
} }

View File

@ -1,32 +1,37 @@
//! Types and functions for building Sapling transaction components. //! Types and functions for building Sapling transaction components.
use core::fmt; use core::fmt;
use std::sync::mpsc::Sender; use std::{marker::PhantomData, sync::mpsc::Sender};
use ff::Field; use ff::Field;
use rand::{seq::SliceRandom, RngCore}; use rand::{seq::SliceRandom, RngCore};
use rand_core::CryptoRng;
use crate::{ use crate::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
keys::OutgoingViewingKey, keys::OutgoingViewingKey,
memo::MemoBytes, memo::MemoBytes,
sapling::{ sapling::{
keys::SaplingIvk, self,
constants::{SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR},
note_encryption::sapling_note_encryption, note_encryption::sapling_note_encryption,
prover::TxProver, prover::{OutputProver, SpendProver},
redjubjub::{PrivateKey, Signature}, redjubjub::{PrivateKey, PublicKey, Signature},
spend_sig_internal, spend_sig_internal,
util::generate_random_rseed_internal, util::generate_random_rseed_internal,
value::{NoteValue, ValueSum}, value::{
Diversifier, MerklePath, Node, Note, PaymentAddress, CommitmentSum, NoteValue, TrapdoorSum, ValueCommitTrapdoor, ValueCommitment, ValueSum,
},
verify_spend_sig, Diversifier, MerklePath, Node, Note, PaymentAddress, ProofGenerationKey,
SaplingIvk,
}, },
transaction::{ transaction::{
builder::Progress, builder::Progress,
components::{ components::{
amount::{Amount, NonNegativeAmount}, amount::{Amount, NonNegativeAmount},
sapling::{ sapling::{
fees, Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription, fees, Authorization, Authorized, Bundle, GrothProofBytes, MapAuth,
SpendDescription, OutputDescription, SpendDescription,
}, },
}, },
}, },
@ -41,8 +46,15 @@ const MIN_SHIELDED_OUTPUTS: usize = 2;
pub enum Error { pub enum Error {
AnchorMismatch, AnchorMismatch,
BindingSig, BindingSig,
/// A signature is valid for more than one input. This should never happen if `alpha`
/// is sampled correctly, and indicates a critical failure in randomness generation.
DuplicateSignature,
InvalidAddress, InvalidAddress,
InvalidAmount, InvalidAmount,
/// External signature is not valid.
InvalidExternalSignature,
/// A bundle could not be built because required signatures were missing.
MissingSignatures,
SpendProof, SpendProof,
} }
@ -53,8 +65,11 @@ impl fmt::Display for Error {
write!(f, "Anchor mismatch (anchors for all spends must be equal)") write!(f, "Anchor mismatch (anchors for all spends must be equal)")
} }
Error::BindingSig => write!(f, "Failed to create bindingSig"), Error::BindingSig => write!(f, "Failed to create bindingSig"),
Error::DuplicateSignature => write!(f, "Signature valid for more than one input"),
Error::InvalidAddress => write!(f, "Invalid address"), Error::InvalidAddress => write!(f, "Invalid address"),
Error::InvalidAmount => write!(f, "Invalid amount"), Error::InvalidAmount => write!(f, "Invalid amount"),
Error::InvalidExternalSignature => write!(f, "External signature was invalid"),
Error::MissingSignatures => write!(f, "Required signatures were missing during build"),
Error::SpendProof => write!(f, "Failed to create Sapling spend proof"), Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
} }
} }
@ -62,11 +77,12 @@ impl fmt::Display for Error {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SpendDescriptionInfo { pub struct SpendDescriptionInfo {
extsk: ExtendedSpendingKey, proof_generation_key: ProofGenerationKey,
diversifier: Diversifier, diversifier: Diversifier,
note: Note, note: Note,
alpha: jubjub::Fr, alpha: jubjub::Fr,
merkle_path: MerklePath, merkle_path: MerklePath,
rcv: ValueCommitTrapdoor,
} }
impl fees::InputView<()> for SpendDescriptionInfo { impl fees::InputView<()> for SpendDescriptionInfo {
@ -81,6 +97,70 @@ impl fees::InputView<()> for SpendDescriptionInfo {
} }
} }
impl SpendDescriptionInfo {
fn new_internal<R: RngCore>(
mut rng: &mut R,
extsk: &ExtendedSpendingKey,
diversifier: Diversifier,
note: Note,
merkle_path: MerklePath,
) -> Self {
SpendDescriptionInfo {
proof_generation_key: extsk.expsk.proof_generation_key(),
diversifier,
note,
alpha: jubjub::Fr::random(&mut rng),
merkle_path,
rcv: ValueCommitTrapdoor::random(rng),
}
}
fn build<Pr: SpendProver>(
self,
anchor: Option<bls12_381::Scalar>,
) -> Result<SpendDescription<InProgress<Unproven, Unsigned>>, Error> {
let anchor = anchor.expect("Sapling anchor must be set if Sapling spends are present.");
// Construct the value commitment.
let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone());
let ak = PublicKey(self.proof_generation_key.ak.into());
// This is the result of the re-randomization, we compute it for the caller
let rk = ak.randomize(self.alpha, SPENDING_KEY_GENERATOR);
let nullifier = self.note.nf(
&self.proof_generation_key.to_viewing_key().nk,
u64::try_from(self.merkle_path.position())
.expect("Sapling note commitment tree position must fit into a u64"),
);
let zkproof = Pr::prepare_circuit(
self.proof_generation_key,
self.diversifier,
*self.note.rseed(),
self.note.value(),
self.alpha,
self.rcv,
anchor,
self.merkle_path.clone(),
)
.ok_or(Error::SpendProof)?;
Ok(SpendDescription {
cv,
anchor,
nullifier,
rk,
zkproof,
spend_auth_sig: SigningParts {
ak,
alpha: self.alpha,
},
})
}
}
/// A struct containing the information required in order to construct a /// A struct containing the information required in order to construct a
/// Sapling output to a transaction. /// Sapling output to a transaction.
#[derive(Clone)] #[derive(Clone)]
@ -89,9 +169,38 @@ struct SaplingOutputInfo {
ovk: Option<OutgoingViewingKey>, ovk: Option<OutgoingViewingKey>,
note: Note, note: Note,
memo: MemoBytes, memo: MemoBytes,
rcv: ValueCommitTrapdoor,
} }
impl SaplingOutputInfo { impl SaplingOutputInfo {
fn dummy<P: consensus::Parameters, R: RngCore>(
params: &P,
mut rng: &mut R,
target_height: BlockHeight,
) -> Self {
// This is a dummy output
let dummy_to = {
let mut diversifier = Diversifier([0; 11]);
loop {
rng.fill_bytes(&mut diversifier.0);
let dummy_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
if let Some(addr) = dummy_ivk.to_payment_address(diversifier) {
break addr;
}
}
};
Self::new_internal(
params,
rng,
target_height,
None,
dummy_to,
NoteValue::from_raw(0),
MemoBytes::empty(),
)
}
fn new_internal<P: consensus::Parameters, R: RngCore>( fn new_internal<P: consensus::Parameters, R: RngCore>(
params: &P, params: &P,
rng: &mut R, rng: &mut R,
@ -105,24 +214,31 @@ impl SaplingOutputInfo {
let note = Note::from_parts(to, value, rseed); let note = Note::from_parts(to, value, rseed);
SaplingOutputInfo { ovk, note, memo } SaplingOutputInfo {
ovk,
note,
memo,
rcv: ValueCommitTrapdoor::random(rng),
}
} }
fn build<P: consensus::Parameters, Pr: TxProver, R: RngCore>( fn build<P: consensus::Parameters, Pr: OutputProver, R: RngCore>(
self, self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R, rng: &mut R,
) -> OutputDescription<GrothProofBytes> { ) -> OutputDescription<sapling::circuit::Output> {
let encryptor = let encryptor =
sapling_note_encryption::<R, P>(self.ovk, self.note.clone(), self.memo, rng); sapling_note_encryption::<R, P>(self.ovk, self.note.clone(), self.memo, rng);
let (zkproof, cv) = prover.output_proof( // Construct the value commitment.
ctx, let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone());
// Prepare the circuit that will be used to construct the proof.
let zkproof = Pr::prepare_circuit(
encryptor.esk().0, encryptor.esk().0,
self.note.recipient(), self.note.recipient(),
self.note.rcm(), self.note.rcm(),
self.note.value().inner(), self.note.value(),
self.rcv,
); );
let cmu = self.note.cmu(); let cmu = self.note.cmu();
@ -197,23 +313,6 @@ pub struct SaplingBuilder<P> {
outputs: Vec<SaplingOutputInfo>, outputs: Vec<SaplingOutputInfo>,
} }
#[derive(Clone)]
pub struct Unauthorized {
tx_metadata: SaplingMetadata,
}
impl std::fmt::Debug for Unauthorized {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "Unauthorized")
}
}
impl Authorization for Unauthorized {
type SpendProof = GrothProofBytes;
type OutputProof = GrothProofBytes;
type AuthSig = SpendDescriptionInfo;
}
impl<P> SaplingBuilder<P> { impl<P> SaplingBuilder<P> {
pub fn new(params: P, target_height: BlockHeight) -> Self { pub fn new(params: P, target_height: BlockHeight) -> Self {
SaplingBuilder { SaplingBuilder {
@ -276,7 +375,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
pub fn add_spend<R: RngCore>( pub fn add_spend<R: RngCore>(
&mut self, &mut self,
mut rng: R, mut rng: R,
extsk: ExtendedSpendingKey, extsk: &ExtendedSpendingKey,
diversifier: Diversifier, diversifier: Diversifier,
note: Note, note: Note,
merkle_path: MerklePath, merkle_path: MerklePath,
@ -292,18 +391,13 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
self.anchor = Some(merkle_path.root(node).into()) self.anchor = Some(merkle_path.root(node).into())
} }
let alpha = jubjub::Fr::random(&mut rng);
self.value_balance = (self.value_balance + note.value()).ok_or(Error::InvalidAmount)?; self.value_balance = (self.value_balance + note.value()).ok_or(Error::InvalidAmount)?;
self.try_value_balance()?; self.try_value_balance()?;
self.spends.push(SpendDescriptionInfo { let spend =
extsk, SpendDescriptionInfo::new_internal(&mut rng, extsk, diversifier, note, merkle_path);
diversifier,
note, self.spends.push(spend);
alpha,
merkle_path,
});
Ok(()) Ok(())
} }
@ -336,22 +430,18 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
Ok(()) Ok(())
} }
pub fn build<Pr: TxProver, R: RngCore>( pub fn build<SP: SpendProver, OP: OutputProver, R: RngCore>(
self, self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
mut rng: R, mut rng: R,
target_height: BlockHeight, target_height: BlockHeight,
progress_notifier: Option<&Sender<Progress>>, ) -> Result<Option<(UnauthorizedBundle, SaplingMetadata)>, Error> {
) -> Result<Option<Bundle<Unauthorized>>, Error> {
let value_balance = self.try_value_balance()?; let value_balance = self.try_value_balance()?;
// Record initial positions of spends and outputs // Record initial positions of spends and outputs
let params = self.params;
let mut indexed_spends: Vec<_> = self.spends.into_iter().enumerate().collect(); let mut indexed_spends: Vec<_> = self.spends.into_iter().enumerate().collect();
let mut indexed_outputs: Vec<_> = self let mut indexed_outputs: Vec<_> = self
.outputs .outputs
.iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, o)| Some((i, o))) .map(|(i, o)| Some((i, o)))
.collect(); .collect();
@ -373,204 +463,403 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
indexed_spends.shuffle(&mut rng); indexed_spends.shuffle(&mut rng);
indexed_outputs.shuffle(&mut rng); indexed_outputs.shuffle(&mut rng);
// Keep track of the total number of steps computed // Record the transaction metadata and create dummy outputs.
let total_progress = indexed_spends.len() as u32 + indexed_outputs.len() as u32; let spend_infos = indexed_spends
let mut progress = 0u32; .into_iter()
.enumerate()
.map(|(i, (pos, spend))| {
// Record the post-randomized spend location
tx_metadata.spend_indices[pos] = i;
// Create Sapling SpendDescriptions spend
let shielded_spends: Vec<SpendDescription<Unauthorized>> = if !indexed_spends.is_empty() { })
let anchor = self .collect::<Vec<_>>();
.anchor let output_infos = indexed_outputs
.expect("Sapling anchor must be set if Sapling spends are present.");
indexed_spends
.into_iter()
.enumerate()
.map(|(i, (pos, spend))| {
let proof_generation_key = spend.extsk.expsk.proof_generation_key();
let nullifier = spend.note.nf(
&proof_generation_key.to_viewing_key().nk,
u64::try_from(spend.merkle_path.position())
.expect("Sapling note commitment tree position must fit into a u64"),
);
let (zkproof, cv, rk) = prover
.spend_proof(
ctx,
proof_generation_key,
spend.diversifier,
*spend.note.rseed(),
spend.alpha,
spend.note.value().inner(),
anchor,
spend.merkle_path.clone(),
)
.map_err(|_| Error::SpendProof)?;
// Record the post-randomized spend location
tx_metadata.spend_indices[pos] = i;
// Update progress and send a notification on the channel
progress += 1;
if let Some(sender) = progress_notifier {
// If the send fails, we should ignore the error, not crash.
sender
.send(Progress::new(progress, Some(total_progress)))
.unwrap_or(());
}
Ok(SpendDescription {
cv,
anchor,
nullifier,
rk,
zkproof,
spend_auth_sig: spend,
})
})
.collect::<Result<Vec<_>, Error>>()?
} else {
vec![]
};
// Create Sapling OutputDescriptions
let shielded_outputs: Vec<OutputDescription<GrothProofBytes>> = indexed_outputs
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, output)| { .map(|(i, output)| {
let result = if let Some((pos, output)) = output { if let Some((pos, output)) = output {
// Record the post-randomized output location // Record the post-randomized output location
tx_metadata.output_indices[pos] = i; tx_metadata.output_indices[pos] = i;
output.clone().build::<P, _, _>(prover, ctx, &mut rng) output
} else { } else {
// This is a dummy output // This is a dummy output
let dummy_note = { SaplingOutputInfo::dummy(&self.params, &mut rng, target_height)
let payment_address = {
let mut diversifier = Diversifier([0; 11]);
loop {
rng.fill_bytes(&mut diversifier.0);
let dummy_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
if let Some(addr) = dummy_ivk.to_payment_address(diversifier) {
break addr;
}
}
};
let rseed =
generate_random_rseed_internal(&params, target_height, &mut rng);
Note::from_parts(payment_address, NoteValue::from_raw(0), rseed)
};
let esk = dummy_note.generate_or_derive_esk_internal(&mut rng);
let epk = esk.derive_public(
dummy_note
.recipient()
.diversifier()
.g_d()
.expect("checked at construction")
.into(),
);
let (zkproof, cv) = prover.output_proof(
ctx,
esk.0,
dummy_note.recipient(),
dummy_note.rcm(),
dummy_note.value().inner(),
);
let cmu = dummy_note.cmu();
let mut enc_ciphertext = [0u8; 580];
let mut out_ciphertext = [0u8; 80];
rng.fill_bytes(&mut enc_ciphertext[..]);
rng.fill_bytes(&mut out_ciphertext[..]);
OutputDescription {
cv,
cmu,
ephemeral_key: epk.to_bytes(),
enc_ciphertext,
out_ciphertext,
zkproof,
}
};
// Update progress and send a notification on the channel
progress += 1;
if let Some(sender) = progress_notifier {
// If the send fails, we should ignore the error, not crash.
sender
.send(Progress::new(progress, Some(total_progress)))
.unwrap_or(());
} }
result
}) })
.collect(); .collect::<Vec<_>>();
// Compute the transaction binding signing key.
let bsk = {
let spends: TrapdoorSum = spend_infos.iter().map(|spend| &spend.rcv).sum();
let outputs: TrapdoorSum = output_infos.iter().map(|output| &output.rcv).sum();
(spends - outputs).into_bsk()
};
// Create the unauthorized Spend and Output descriptions.
let shielded_spends = spend_infos
.into_iter()
.map(|a| a.build::<SP>(self.anchor))
.collect::<Result<Vec<_>, _>>()?;
let shielded_outputs = output_infos
.into_iter()
.map(|a| a.build::<P, OP, _>(&mut rng))
.collect::<Vec<_>>();
// Verify that bsk and bvk are consistent.
let bvk = {
let spends = shielded_spends
.iter()
.map(|spend| spend.cv())
.sum::<CommitmentSum>();
let outputs = shielded_outputs
.iter()
.map(|output| output.cv())
.sum::<CommitmentSum>();
(spends - outputs)
.into_bvk(i64::try_from(self.value_balance).map_err(|_| Error::InvalidAmount)?)
};
assert_eq!(
PublicKey::from_private(&bsk, VALUE_COMMITMENT_RANDOMNESS_GENERATOR).0,
bvk.0,
);
let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() { let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() {
None None
} else { } else {
Some(Bundle { Some((
shielded_spends, Bundle {
shielded_outputs, shielded_spends,
value_balance, shielded_outputs,
authorization: Unauthorized { tx_metadata }, value_balance,
}) authorization: InProgress {
sigs: Unsigned { bsk },
_proof_state: PhantomData::default(),
},
},
tx_metadata,
))
}; };
Ok(bundle) Ok(bundle)
} }
} }
impl SpendDescription<Unauthorized> { /// Type alias for an in-progress bundle that has no proofs or signatures.
pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription<Authorized> { ///
SpendDescription { /// This is returned by [`SaplingBuilder::build`].
cv: self.cv.clone(), pub type UnauthorizedBundle = Bundle<InProgress<Unproven, Unsigned>>;
anchor: self.anchor,
nullifier: self.nullifier, /// Marker trait representing bundle proofs in the process of being created.
rk: self.rk.clone(), pub trait InProgressProofs: fmt::Debug {
zkproof: self.zkproof, /// The proof type of a Sapling spend in the process of being proven.
spend_auth_sig, type SpendProof: Clone + fmt::Debug;
/// The proof type of a Sapling output in the process of being proven.
type OutputProof: Clone + fmt::Debug;
}
/// Marker trait representing bundle signatures in the process of being created.
pub trait InProgressSignatures: fmt::Debug {
/// The authorization type of a Sapling spend or output in the process of being
/// authorized.
type AuthSig: Clone + fmt::Debug;
}
/// Marker for a bundle in the process of being built.
#[derive(Clone, Debug)]
pub struct InProgress<P: InProgressProofs, S: InProgressSignatures> {
sigs: S,
_proof_state: PhantomData<P>,
}
impl<P: InProgressProofs, S: InProgressSignatures> Authorization for InProgress<P, S> {
type SpendProof = P::SpendProof;
type OutputProof = P::OutputProof;
type AuthSig = S::AuthSig;
}
/// Marker for a [`Bundle`] without proofs.
///
/// The [`SpendDescription`]s and [`OutputDescription`]s within the bundle contain the
/// private data needed to create proofs.
#[derive(Debug)]
pub struct Unproven;
impl InProgressProofs for Unproven {
type SpendProof = sapling::circuit::Spend;
type OutputProof = sapling::circuit::Output;
}
/// Marker for a [`Bundle`] with proofs.
#[derive(Debug)]
pub struct Proven;
impl InProgressProofs for Proven {
type SpendProof = GrothProofBytes;
type OutputProof = GrothProofBytes;
}
struct CreateProofs<'a, SP: SpendProver, OP: OutputProver, R: RngCore> {
spend_prover: &'a SP,
output_prover: &'a OP,
rng: R,
progress_notifier: Option<&'a Sender<Progress>>,
total_progress: u32,
progress: u32,
}
impl<'a, SP: SpendProver, OP: OutputProver, R: RngCore> CreateProofs<'a, SP, OP, R> {
fn new(
spend_prover: &'a SP,
output_prover: &'a OP,
rng: R,
progress_notifier: Option<&'a Sender<Progress>>,
total_progress: u32,
) -> Self {
// Keep track of the total number of steps computed
Self {
spend_prover,
output_prover,
rng,
progress_notifier,
total_progress,
progress: 0u32,
}
}
fn update_progress(&mut self) {
// Update progress and send a notification on the channel
self.progress += 1;
if let Some(sender) = self.progress_notifier {
// If the send fails, we should ignore the error, not crash.
sender
.send(Progress::new(self.progress, Some(self.total_progress)))
.unwrap_or(());
} }
} }
} }
impl Bundle<Unauthorized> { impl<'a, S: InProgressSignatures, SP: SpendProver, OP: OutputProver, R: RngCore>
pub fn apply_signatures<Pr: TxProver, R: RngCore>( MapAuth<InProgress<Unproven, S>, InProgress<Proven, S>> for CreateProofs<'a, SP, OP, R>
self, {
prover: &Pr, fn map_spend_proof(&mut self, spend: sapling::circuit::Spend) -> GrothProofBytes {
ctx: &mut Pr::SaplingProvingContext, let proof = self.spend_prover.create_proof(spend, &mut self.rng);
rng: &mut R, self.update_progress();
sighash_bytes: &[u8; 32], SP::encode_proof(proof)
) -> Result<(Bundle<Authorized>, SaplingMetadata), Error> { }
let binding_sig = prover
.binding_sig(ctx, self.value_balance, sighash_bytes)
.map_err(|_| Error::BindingSig)?;
Ok(( fn map_output_proof(&mut self, output: sapling::circuit::Output) -> GrothProofBytes {
Bundle { let proof = self.output_prover.create_proof(output, &mut self.rng);
shielded_spends: self self.update_progress();
.shielded_spends OP::encode_proof(proof)
.iter() }
.map(|spend| {
spend.apply_signature(spend_sig_internal( fn map_auth_sig(&mut self, s: S::AuthSig) -> S::AuthSig {
PrivateKey(spend.spend_auth_sig.extsk.expsk.ask), s
spend.spend_auth_sig.alpha, }
sighash_bytes,
rng, fn map_authorization(&mut self, a: InProgress<Unproven, S>) -> InProgress<Proven, S> {
)) InProgress {
}) sigs: a.sigs,
.collect(), _proof_state: PhantomData::default(),
shielded_outputs: self.shielded_outputs, }
value_balance: self.value_balance, }
authorization: Authorized { binding_sig }, }
impl<S: InProgressSignatures> Bundle<InProgress<Unproven, S>> {
/// Creates the proofs for this bundle.
pub fn create_proofs<SP: SpendProver, OP: OutputProver>(
self,
spend_prover: &SP,
output_prover: &OP,
rng: impl RngCore,
progress_notifier: Option<&Sender<Progress>>,
) -> Bundle<InProgress<Proven, S>> {
let total_progress = self.shielded_spends.len() as u32 + self.shielded_outputs.len() as u32;
self.map_authorization(CreateProofs::new(
spend_prover,
output_prover,
rng,
progress_notifier,
total_progress,
))
}
}
/// Marker for an unauthorized bundle with no signatures.
pub struct Unsigned {
bsk: PrivateKey,
}
impl fmt::Debug for Unsigned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Unsigned").finish_non_exhaustive()
}
}
impl InProgressSignatures for Unsigned {
type AuthSig = SigningParts;
}
/// The parts needed to sign a [`SpendDescription`].
#[derive(Clone, Debug)]
pub struct SigningParts {
/// The spend validating key for this spend description. Used to match spend
/// authorizing keys to spend descriptions they can create signatures for.
ak: PublicKey,
/// The randomization needed to derive the actual signing key for this note.
alpha: jubjub::Scalar,
}
/// Marker for a partially-authorized bundle, in the process of being signed.
#[derive(Debug)]
pub struct PartiallyAuthorized {
binding_signature: Signature,
sighash: [u8; 32],
}
impl InProgressSignatures for PartiallyAuthorized {
type AuthSig = MaybeSigned;
}
/// A heisen[`Signature`] for a particular [`SpendDescription`].
#[derive(Clone, Debug)]
pub enum MaybeSigned {
/// The information needed to sign this [`SpendDescription`].
SigningMetadata(SigningParts),
/// The signature for this [`SpendDescription`].
Signature(Signature),
}
impl MaybeSigned {
fn finalize(self) -> Result<Signature, Error> {
match self {
Self::Signature(sig) => Ok(sig),
_ => Err(Error::MissingSignatures),
}
}
}
impl<P: InProgressProofs> Bundle<InProgress<P, Unsigned>> {
/// Loads the sighash into this bundle, preparing it for signing.
///
/// This API ensures that all signatures are created over the same sighash.
pub fn prepare<R: RngCore + CryptoRng>(
self,
mut rng: R,
sighash: [u8; 32],
) -> Bundle<InProgress<P, PartiallyAuthorized>> {
self.map_authorization((
|proof| proof,
|proof| proof,
MaybeSigned::SigningMetadata,
|auth: InProgress<P, Unsigned>| InProgress {
sigs: PartiallyAuthorized {
binding_signature: auth.sigs.bsk.sign(
&sighash,
&mut rng,
VALUE_COMMITMENT_RANDOMNESS_GENERATOR,
),
sighash,
},
_proof_state: PhantomData::default(),
},
))
}
}
impl Bundle<InProgress<Proven, Unsigned>> {
/// Applies signatures to this bundle, in order to authorize it.
///
/// This is a helper method that wraps [`Bundle::prepare`], [`Bundle::sign`], and
/// [`Bundle::finalize`].
pub fn apply_signatures<R: RngCore + CryptoRng>(
self,
mut rng: R,
sighash: [u8; 32],
signing_keys: &[PrivateKey],
) -> Result<Bundle<Authorized>, Error> {
signing_keys
.iter()
.fold(self.prepare(&mut rng, sighash), |partial, ask| {
partial.sign(&mut rng, ask)
})
.finalize()
}
}
impl<P: InProgressProofs> Bundle<InProgress<P, PartiallyAuthorized>> {
/// Signs this bundle with the given [`PrivateKey`].
///
/// This will apply signatures for all notes controlled by this spending key.
pub fn sign<R: RngCore + CryptoRng>(self, mut rng: R, ask: &PrivateKey) -> Self {
let expected_ak = PublicKey::from_private(ask, SPENDING_KEY_GENERATOR);
let sighash = self.authorization.sigs.sighash;
self.map_authorization((
|proof| proof,
|proof| proof,
|maybe| match maybe {
MaybeSigned::SigningMetadata(parts) if parts.ak.0 == expected_ak.0 => {
MaybeSigned::Signature(spend_sig_internal(ask, parts.alpha, &sighash, &mut rng))
}
s => s,
},
|partial| partial,
))
}
/// Appends externally computed [`Signature`]s.
///
/// Each signature will be applied to the one input for which it is valid. An error
/// will be returned if the signature is not valid for any inputs, or if it is valid
/// for more than one input.
pub fn append_signatures(self, signatures: &[Signature]) -> Result<Self, Error> {
signatures.iter().try_fold(self, Self::append_signature)
}
fn append_signature(self, signature: &Signature) -> Result<Self, Error> {
let sighash = self.authorization.sigs.sighash;
let mut signature_valid_for = 0usize;
let bundle = self.map_authorization((
|proof| proof,
|proof| proof,
|maybe| match maybe {
MaybeSigned::SigningMetadata(parts) => {
if verify_spend_sig(&parts.ak, parts.alpha, &sighash, signature) {
signature_valid_for += 1;
MaybeSigned::Signature(*signature)
} else {
// Signature isn't for this input.
MaybeSigned::SigningMetadata(parts)
}
}
s => s,
},
|partial| partial,
));
match signature_valid_for {
0 => Err(Error::InvalidExternalSignature),
1 => Ok(bundle),
_ => Err(Error::DuplicateSignature),
}
}
}
impl Bundle<InProgress<Proven, 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> {
self.try_map_authorization((
Ok,
Ok,
|maybe: MaybeSigned| maybe.finalize(),
|partial: InProgress<Proven, PartiallyAuthorized>| {
Ok(Authorized {
binding_sig: partial.sigs.binding_signature,
})
}, },
self.authorization.tx_metadata,
)) ))
} }
} }
@ -587,7 +876,8 @@ pub mod testing {
TEST_NETWORK, TEST_NETWORK,
}, },
sapling::{ sapling::{
prover::mock::MockTxProver, prover::mock::{MockOutputProver, MockSpendProver},
redjubjub::PrivateKey,
testing::{arb_node, arb_note}, testing::{arb_node, arb_note},
value::testing::arb_positive_note_value, value::testing::arb_positive_note_value,
Diversifier, Diversifier,
@ -628,31 +918,30 @@ pub mod testing {
for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) { for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) {
builder.add_spend( builder.add_spend(
&mut rng, &mut rng,
extsk.clone(), &extsk,
diversifier, diversifier,
note, note,
path path
).unwrap(); ).unwrap();
} }
let prover = MockTxProver; let (bundle, _) = builder.build::<MockSpendProver, MockOutputProver, _>(
let bundle = builder.build(
&prover,
&mut (),
&mut rng, &mut rng,
target_height.unwrap(), target_height.unwrap(),
None
).unwrap().unwrap(); ).unwrap().unwrap();
let (bundle, _) = bundle.apply_signatures( let bundle = bundle.create_proofs(
&prover, &MockSpendProver,
&mut (), &MockOutputProver,
&mut rng, &mut rng,
&fake_sighash_bytes, None,
).unwrap(); );
bundle bundle.apply_signatures(
&mut rng,
fake_sighash_bytes,
&[PrivateKey(extsk.expsk.ask)],
).unwrap()
} }
} }
} }

View File

@ -275,7 +275,8 @@ pub struct Unauthorized;
impl Authorization for Unauthorized { impl Authorization for Unauthorized {
type TransparentAuth = transparent::builder::Unauthorized; type TransparentAuth = transparent::builder::Unauthorized;
type SaplingAuth = sapling::builder::Unauthorized; type SaplingAuth =
sapling::builder::InProgress<sapling::builder::Proven, sapling::builder::Unsigned>;
type OrchardAuth = type OrchardAuth =
orchard::builder::InProgress<orchard::builder::Unproven, orchard::builder::Unauthorized>; orchard::builder::InProgress<orchard::builder::Unproven, orchard::builder::Unauthorized>;