Refactor Sapling builder to separate out proof generation

Closes zcash/librustzcash#741.
This commit is contained in:
Jack Grigg 2023-05-01 22:10:00 +00:00
parent 13fb0a4819
commit b2ff29db78
12 changed files with 711 additions and 319 deletions

View File

@ -8,7 +8,7 @@ use zcash_primitives::{
sapling::{
self,
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::TxProver as SaplingProver,
prover::{OutputProver, SpendProver},
Node,
},
transaction::{
@ -191,7 +191,8 @@ where
pub fn create_spend_to_address<DbT, ParamsT>(
wallet_db: &mut DbT,
params: &ParamsT,
prover: impl SaplingProver,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey,
to: &RecipientAddress,
amount: NonNegativeAmount,
@ -231,7 +232,15 @@ where
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
@ -289,7 +298,8 @@ where
pub fn spend<DbT, ParamsT, InputsT>(
wallet_db: &mut DbT,
params: &ParamsT,
prover: impl SaplingProver,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT,
usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest,
@ -324,7 +334,15 @@ where
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
@ -475,7 +493,8 @@ where
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT>(
wallet_db: &mut DbT,
params: &ParamsT,
prover: impl SaplingProver,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, DbT::NoteRef>,
@ -663,7 +682,8 @@ where
}
// 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 sapling_outputs =
@ -768,7 +788,8 @@ where
pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
wallet_db: &mut DbT,
params: &ParamsT,
prover: impl SaplingProver,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT,
shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey,
@ -798,7 +819,15 @@ where
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)]

View File

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

View File

@ -451,8 +451,9 @@ pub(crate) mod tests {
legacy::TransparentAddress,
memo::{Memo, MemoBytes},
sapling::{
note_encryption::try_sapling_output_recovery, prover::TxProver, Node, Note,
PaymentAddress,
note_encryption::try_sapling_output_recovery,
prover::{OutputProver, SpendProver},
Node, Note, PaymentAddress,
},
transaction::{
components::{amount::NonNegativeAmount, Amount},
@ -497,7 +498,7 @@ pub(crate) mod tests {
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() {
Some(tx_prover) => tx_prover,
None => {

View File

@ -839,7 +839,7 @@ mod tests {
.unwrap();
let (tx_a, _) = builder_a
.txn_builder
.build_zfuture(&prover, &fee_rule)
.build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e))
.unwrap();
let tze_a = tx_a.tze_bundle().unwrap();
@ -857,7 +857,7 @@ mod tests {
.unwrap();
let (tx_b, _) = builder_b
.txn_builder
.build_zfuture(&prover, &fee_rule)
.build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e))
.unwrap();
let tze_b = tx_b.tze_bundle().unwrap();
@ -882,7 +882,7 @@ mod tests {
let (tx_c, _) = builder_c
.txn_builder
.build_zfuture(&prover, &fee_rule)
.build_zfuture(&prover, &prover, &fee_rule)
.map_err(|e| format!("build failure: {:?}", e))
.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,
PreparedIncomingViewingKey, SaplingDomain,
},
prover::mock::MockTxProver,
prover::mock::{MockOutputProver, MockSpendProver},
value::NoteValue,
Diversifier, SaplingIvk,
},
transaction::components::sapling::{
builder::SaplingBuilder, CompactOutputDescription, GrothProofBytes, OutputDescription,
},
transaction::components::sapling::{builder::SaplingBuilder, CompactOutputDescription},
};
#[cfg(unix)]
@ -32,7 +30,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
// Construct a Sapling output.
let output: OutputDescription<GrothProofBytes> = {
let output = {
let diversifier = Diversifier([0; 11]);
let pa = valid_ivk.to_payment_address(diversifier).unwrap();
@ -46,8 +44,8 @@ fn bench_note_decryption(c: &mut Criterion) {
MemoBytes::empty(),
)
.unwrap();
let bundle = builder
.build(&MockTxProver, &mut (), &mut rng, height, None)
let (bundle, _) = builder
.build::<MockSpendProver, MockOutputProver, _>(&mut rng, height)
.unwrap()
.unwrap();
bundle.shielded_outputs()[0].clone()

View File

@ -36,11 +36,11 @@ pub fn spend_sig<R: RngCore + CryptoRng>(
sighash: &[u8; 32],
rng: &mut R,
) -> Signature {
spend_sig_internal(ask, ar, sighash, rng)
spend_sig_internal(&ask, ar, sighash, rng)
}
pub(crate) fn spend_sig_internal<R: RngCore>(
ask: PrivateKey,
ask: &PrivateKey,
ar: jubjub::Fr,
sighash: &[u8; 32],
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)
}
/// 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"))]
pub mod testing {
pub use super::{

View File

@ -38,7 +38,7 @@
//! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html
use bitvec::{array::BitArray, order::Lsb0};
use ff::Field;
use ff::{Field, PrimeField};
use group::GroupEncoding;
use rand::RngCore;
use subtle::CtOption;
@ -86,6 +86,21 @@ impl ValueCommitTrapdoor {
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.
///
/// 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 {
fn sub_assign(&mut self, rhs: &ValueCommitTrapdoor) {
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 {
fn sum<I: Iterator<Item = ValueCommitment>>(iter: I) -> Self {
iter.fold(CommitmentSum::zero(), |acc, cv| acc + &cv)

View File

@ -12,7 +12,11 @@ use crate::{
keys::OutgoingViewingKey,
legacy::TransparentAddress,
memo::MemoBytes,
sapling::{self, prover::TxProver, Diversifier, Note, PaymentAddress},
sapling::{
self,
prover::{OutputProver, SpendProver},
redjubjub, Diversifier, Note, PaymentAddress,
},
transaction::{
components::{
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
// transaction, and then the caller will be responsible for using the spending keys or their
// derivatives for proving and signing to complete transaction creation.
sapling_asks: Vec<redjubjub::PrivateKey>,
orchard_saks: Vec<orchard::keys::SpendAuthorizingKey>,
#[cfg(feature = "zfuture")]
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(),
sapling_builder: SaplingBuilder::new(params, target_height),
orchard_builder,
sapling_asks: vec![],
orchard_saks: Vec::new(),
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(),
@ -329,7 +335,12 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
merkle_path: sapling::MerklePath,
) -> Result<(), sapling_builder::Error> {
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.
@ -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
/// [`SaplingMetadata`] generated during the build process.
pub fn build<FR: FeeRule>(
pub fn build<SP: SpendProver, OP: OutputProver, FR: FeeRule>(
self,
prover: &impl TxProver,
spend_prover: &SP,
output_prover: &OP,
fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
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.
@ -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
/// [`SaplingMetadata`] generated during the build process.
#[cfg(feature = "zfuture")]
pub fn build_zfuture<FR: FutureFeeRule>(
pub fn build_zfuture<SP: SpendProver, OP: OutputProver, FR: FutureFeeRule>(
self,
prover: &impl TxProver,
spend_prover: &SP,
output_prover: &OP,
fee_rule: &FR,
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
let fee = fee_rule
@ -464,12 +477,13 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
)
.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,
prover: &impl TxProver,
spend_prover: &SP,
output_prover: &OP,
fee: Amount,
) -> Result<(Transaction, SaplingMetadata), Error<FE>> {
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 mut rng = self.rng;
let mut ctx = prover.new_sapling_proving_context();
let sapling_bundle = self
let (sapling_bundle, tx_metadata) = match self
.sapling_builder
.build(
prover,
&mut ctx,
&mut rng,
self.target_height,
self.progress_notifier.as_ref(),
)
.map_err(Error::SaplingBuild)?;
.build::<SP, OP, _>(&mut rng, self.target_height)
.map_err(Error::SaplingBuild)?
.map(|(bundle, tx_metadata)| {
// We need to create proofs before signatures, because we still support
// creating V4 transactions, which commit to the Sapling proofs in the
// transaction digest.
(
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>> =
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 =
signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
let (sapling_bundle, tx_metadata) = match unauthed_tx
let sapling_bundle = unauthed_tx
.sapling_bundle
.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()
.map_err(Error::SaplingBuild)?
{
Some((bundle, meta)) => (Some(bundle), meta),
None => (None, SaplingMetadata::empty()),
};
.map_err(Error::SaplingBuild)?;
let orchard_bundle = unauthed_tx
.orchard_bundle
@ -648,7 +672,7 @@ mod testing {
use super::{Builder, Error, SaplingMetadata};
use crate::{
consensus::{self, BlockHeight},
sapling::prover::mock::MockTxProver,
sapling::prover::mock::{MockOutputProver, MockSpendProver},
transaction::fees::fixed,
transaction::Transaction,
};
@ -693,7 +717,11 @@ mod testing {
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> {
#[allow(deprecated)]
self.build(&MockTxProver, &fixed::FeeRule::standard())
self.build(
&MockSpendProver,
&MockOutputProver,
&fixed::FeeRule::standard(),
)
}
}
}
@ -710,10 +738,7 @@ mod tests {
legacy::TransparentAddress,
memo::MemoBytes,
sapling::{self, Node, Rseed},
transaction::components::{
amount::{Amount, NonNegativeAmount},
sapling::builder::{self as sapling_builder},
},
transaction::components::amount::{Amount, BalanceError, NonNegativeAmount},
zip32::ExtendedSpendingKey,
};
@ -759,6 +784,7 @@ mod tests {
tze_builder: std::marker::PhantomData,
progress_notifier: None,
orchard_builder: None,
sapling_asks: vec![],
orchard_saks: Vec::new(),
};
@ -828,12 +854,9 @@ mod tests {
)
.unwrap();
// Expect a binding signature error, because our inputs aren't valid, but this shows
// that a binding signature was attempted
assert_matches!(
builder.mock_build(),
Err(Error::SaplingBuild(sapling_builder::Error::BindingSig))
);
// A binding signature (and bundle) is present because there is a Sapling spend.
let (tx, _) = builder.mock_build().unwrap();
assert!(tx.sapling_bundle().is_some());
}
#[test]
@ -950,9 +973,6 @@ mod tests {
// 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
//
// (Still fails because we are using a MockTxProver which doesn't correctly
// compute bindingSig.)
{
let mut builder = Builder::new(TEST_NETWORK, tx_height, None);
builder
@ -982,8 +1002,8 @@ mod tests {
.unwrap();
assert_matches!(
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;
}
#[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
/// the ledger.
#[derive(Debug, Copy, Clone)]
pub struct Authorized {
// TODO: Make this private.
pub binding_sig: redjubjub::Signature,
}

View File

@ -1,32 +1,37 @@
//! Types and functions for building Sapling transaction components.
use core::fmt;
use std::sync::mpsc::Sender;
use std::{marker::PhantomData, sync::mpsc::Sender};
use ff::Field;
use rand::{seq::SliceRandom, RngCore};
use rand_core::CryptoRng;
use crate::{
consensus::{self, BlockHeight},
keys::OutgoingViewingKey,
memo::MemoBytes,
sapling::{
keys::SaplingIvk,
self,
constants::{SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR},
note_encryption::sapling_note_encryption,
prover::TxProver,
redjubjub::{PrivateKey, Signature},
prover::{OutputProver, SpendProver},
redjubjub::{PrivateKey, PublicKey, Signature},
spend_sig_internal,
util::generate_random_rseed_internal,
value::{NoteValue, ValueSum},
Diversifier, MerklePath, Node, Note, PaymentAddress,
value::{
CommitmentSum, NoteValue, TrapdoorSum, ValueCommitTrapdoor, ValueCommitment, ValueSum,
},
verify_spend_sig, Diversifier, MerklePath, Node, Note, PaymentAddress, ProofGenerationKey,
SaplingIvk,
},
transaction::{
builder::Progress,
components::{
amount::{Amount, NonNegativeAmount},
sapling::{
fees, Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription,
SpendDescription,
fees, Authorization, Authorized, Bundle, GrothProofBytes, MapAuth,
OutputDescription, SpendDescription,
},
},
},
@ -41,8 +46,15 @@ const MIN_SHIELDED_OUTPUTS: usize = 2;
pub enum Error {
AnchorMismatch,
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,
InvalidAmount,
/// External signature is not valid.
InvalidExternalSignature,
/// A bundle could not be built because required signatures were missing.
MissingSignatures,
SpendProof,
}
@ -53,8 +65,11 @@ impl fmt::Display for Error {
write!(f, "Anchor mismatch (anchors for all spends must be equal)")
}
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::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"),
}
}
@ -62,11 +77,12 @@ impl fmt::Display for Error {
#[derive(Debug, Clone)]
pub struct SpendDescriptionInfo {
extsk: ExtendedSpendingKey,
proof_generation_key: ProofGenerationKey,
diversifier: Diversifier,
note: Note,
alpha: jubjub::Fr,
merkle_path: MerklePath,
rcv: ValueCommitTrapdoor,
}
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
/// Sapling output to a transaction.
#[derive(Clone)]
@ -89,9 +169,38 @@ struct SaplingOutputInfo {
ovk: Option<OutgoingViewingKey>,
note: Note,
memo: MemoBytes,
rcv: ValueCommitTrapdoor,
}
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>(
params: &P,
rng: &mut R,
@ -105,24 +214,31 @@ impl SaplingOutputInfo {
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,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R,
) -> OutputDescription<GrothProofBytes> {
) -> OutputDescription<sapling::circuit::Output> {
let encryptor =
sapling_note_encryption::<R, P>(self.ovk, self.note.clone(), self.memo, rng);
let (zkproof, cv) = prover.output_proof(
ctx,
// Construct the value commitment.
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,
self.note.recipient(),
self.note.rcm(),
self.note.value().inner(),
self.note.value(),
self.rcv,
);
let cmu = self.note.cmu();
@ -197,23 +313,6 @@ pub struct SaplingBuilder<P> {
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> {
pub fn new(params: P, target_height: BlockHeight) -> Self {
SaplingBuilder {
@ -276,7 +375,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
pub fn add_spend<R: RngCore>(
&mut self,
mut rng: R,
extsk: ExtendedSpendingKey,
extsk: &ExtendedSpendingKey,
diversifier: Diversifier,
note: Note,
merkle_path: MerklePath,
@ -292,18 +391,13 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
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.try_value_balance()?;
self.spends.push(SpendDescriptionInfo {
extsk,
diversifier,
note,
alpha,
merkle_path,
});
let spend =
SpendDescriptionInfo::new_internal(&mut rng, extsk, diversifier, note, merkle_path);
self.spends.push(spend);
Ok(())
}
@ -336,22 +430,18 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
Ok(())
}
pub fn build<Pr: TxProver, R: RngCore>(
pub fn build<SP: SpendProver, OP: OutputProver, R: RngCore>(
self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
mut rng: R,
target_height: BlockHeight,
progress_notifier: Option<&Sender<Progress>>,
) -> Result<Option<Bundle<Unauthorized>>, Error> {
) -> Result<Option<(UnauthorizedBundle, SaplingMetadata)>, Error> {
let value_balance = self.try_value_balance()?;
// 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_outputs: Vec<_> = self
.outputs
.iter()
.into_iter()
.enumerate()
.map(|(i, o)| Some((i, o)))
.collect();
@ -373,204 +463,403 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
indexed_spends.shuffle(&mut rng);
indexed_outputs.shuffle(&mut rng);
// Keep track of the total number of steps computed
let total_progress = indexed_spends.len() as u32 + indexed_outputs.len() as u32;
let mut progress = 0u32;
// Record the transaction metadata and create dummy outputs.
let spend_infos = indexed_spends
.into_iter()
.enumerate()
.map(|(i, (pos, spend))| {
// Record the post-randomized spend location
tx_metadata.spend_indices[pos] = i;
// Create Sapling SpendDescriptions
let shielded_spends: Vec<SpendDescription<Unauthorized>> = if !indexed_spends.is_empty() {
let anchor = self
.anchor
.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
spend
})
.collect::<Vec<_>>();
let output_infos = indexed_outputs
.into_iter()
.enumerate()
.map(|(i, output)| {
let result = if let Some((pos, output)) = output {
if let Some((pos, output)) = output {
// Record the post-randomized output location
tx_metadata.output_indices[pos] = i;
output.clone().build::<P, _, _>(prover, ctx, &mut rng)
output
} else {
// This is a dummy output
let dummy_note = {
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(());
SaplingOutputInfo::dummy(&self.params, &mut rng, target_height)
}
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() {
None
} else {
Some(Bundle {
shielded_spends,
shielded_outputs,
value_balance,
authorization: Unauthorized { tx_metadata },
})
Some((
Bundle {
shielded_spends,
shielded_outputs,
value_balance,
authorization: InProgress {
sigs: Unsigned { bsk },
_proof_state: PhantomData::default(),
},
},
tx_metadata,
))
};
Ok(bundle)
}
}
impl SpendDescription<Unauthorized> {
pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription<Authorized> {
SpendDescription {
cv: self.cv.clone(),
anchor: self.anchor,
nullifier: self.nullifier,
rk: self.rk.clone(),
zkproof: self.zkproof,
spend_auth_sig,
/// Type alias for an in-progress bundle that has no proofs or signatures.
///
/// This is returned by [`SaplingBuilder::build`].
pub type UnauthorizedBundle = Bundle<InProgress<Unproven, Unsigned>>;
/// Marker trait representing bundle proofs in the process of being created.
pub trait InProgressProofs: fmt::Debug {
/// The proof type of a Sapling spend in the process of being proven.
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> {
pub fn apply_signatures<Pr: TxProver, R: RngCore>(
self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R,
sighash_bytes: &[u8; 32],
) -> Result<(Bundle<Authorized>, SaplingMetadata), Error> {
let binding_sig = prover
.binding_sig(ctx, self.value_balance, sighash_bytes)
.map_err(|_| Error::BindingSig)?;
impl<'a, S: InProgressSignatures, SP: SpendProver, OP: OutputProver, R: RngCore>
MapAuth<InProgress<Unproven, S>, InProgress<Proven, S>> for CreateProofs<'a, SP, OP, R>
{
fn map_spend_proof(&mut self, spend: sapling::circuit::Spend) -> GrothProofBytes {
let proof = self.spend_prover.create_proof(spend, &mut self.rng);
self.update_progress();
SP::encode_proof(proof)
}
Ok((
Bundle {
shielded_spends: self
.shielded_spends
.iter()
.map(|spend| {
spend.apply_signature(spend_sig_internal(
PrivateKey(spend.spend_auth_sig.extsk.expsk.ask),
spend.spend_auth_sig.alpha,
sighash_bytes,
rng,
))
})
.collect(),
shielded_outputs: self.shielded_outputs,
value_balance: self.value_balance,
authorization: Authorized { binding_sig },
fn map_output_proof(&mut self, output: sapling::circuit::Output) -> GrothProofBytes {
let proof = self.output_prover.create_proof(output, &mut self.rng);
self.update_progress();
OP::encode_proof(proof)
}
fn map_auth_sig(&mut self, s: S::AuthSig) -> S::AuthSig {
s
}
fn map_authorization(&mut self, a: InProgress<Unproven, S>) -> InProgress<Proven, S> {
InProgress {
sigs: a.sigs,
_proof_state: PhantomData::default(),
}
}
}
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,
},
sapling::{
prover::mock::MockTxProver,
prover::mock::{MockOutputProver, MockSpendProver},
redjubjub::PrivateKey,
testing::{arb_node, arb_note},
value::testing::arb_positive_note_value,
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()) {
builder.add_spend(
&mut rng,
extsk.clone(),
&extsk,
diversifier,
note,
path
).unwrap();
}
let prover = MockTxProver;
let bundle = builder.build(
&prover,
&mut (),
let (bundle, _) = builder.build::<MockSpendProver, MockOutputProver, _>(
&mut rng,
target_height.unwrap(),
None
).unwrap().unwrap();
let (bundle, _) = bundle.apply_signatures(
&prover,
&mut (),
let bundle = bundle.create_proofs(
&MockSpendProver,
&MockOutputProver,
&mut rng,
&fake_sighash_bytes,
).unwrap();
None,
);
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 {
type TransparentAuth = transparent::builder::Unauthorized;
type SaplingAuth = sapling::builder::Unauthorized;
type SaplingAuth =
sapling::builder::InProgress<sapling::builder::Proven, sapling::builder::Unsigned>;
type OrchardAuth =
orchard::builder::InProgress<orchard::builder::Unproven, orchard::builder::Unauthorized>;