Merge pull request #1023 from zcash/741-sapling-builder-refactor
Refactor Sapling builder to separate out proof generation
This commit is contained in:
commit
44c64e1fb9
|
@ -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)]
|
||||
|
|
|
@ -448,10 +448,12 @@ impl<Cache> TestState<Cache> {
|
|||
>,
|
||||
> {
|
||||
let params = self.network();
|
||||
let prover = test_prover();
|
||||
create_spend_to_address(
|
||||
&mut self.db_data,
|
||||
¶ms,
|
||||
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,
|
||||
¶ms,
|
||||
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,
|
||||
¶ms,
|
||||
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,
|
||||
¶ms,
|
||||
test_prover(),
|
||||
&prover,
|
||||
&prover,
|
||||
input_selector,
|
||||
shielding_threshold,
|
||||
usk,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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(¶ms, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
Loading…
Reference in New Issue