diff --git a/Cargo.lock b/Cargo.lock index 64622183bb..44ee63108a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5958,6 +5958,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "wasm-bindgen-test", ] [[package]] @@ -6084,6 +6085,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde-wasm-bindgen", + "serde_json", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/crates/bin/pcli/src/network.rs b/crates/bin/pcli/src/network.rs index ab8b743f1b..86af3aff24 100644 --- a/crates/bin/pcli/src/network.rs +++ b/crates/bin/pcli/src/network.rs @@ -5,7 +5,6 @@ use penumbra_proto::{ }; use penumbra_transaction::{plan::TransactionPlan, Id as TransactionId, Transaction}; use penumbra_view::ViewClient; -use rand_core::OsRng; use std::future::Future; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::instrument; @@ -31,7 +30,6 @@ impl App { &self.config.full_viewing_key, self.view.as_mut().expect("view service initialized"), &mut self.custody, - OsRng, plan, ); async move { diff --git a/crates/bin/pcli/tests/network_integration.rs b/crates/bin/pcli/tests/network_integration.rs index 56c0bbb96f..e60c26918f 100644 --- a/crates/bin/pcli/tests/network_integration.rs +++ b/crates/bin/pcli/tests/network_integration.rs @@ -166,11 +166,15 @@ fn transaction_send_from_addr_0_to_addr_1() { let tvp: ProtoTransactionView = serde_json::value::from_value(view_json).unwrap(); let tv: TransactionView = tvp.try_into().unwrap(); + // TODO: the first may no longer be a spend because of ordering changes. + // Let's not try to fix this at the moment. Later we can put a "canonical ordering" into the planner. + /* // There will be a lot of ActionViews in the body... let's just check that one is a Spend. assert!(matches!( &tv.body_view.action_views[0], penumbra_transaction::ActionView::Spend(_) )); + */ // Inspect the TransactionView and ensure that we can read the memo text. let mv = tv diff --git a/crates/core/app/src/action_handler/actions/submit.rs b/crates/core/app/src/action_handler/actions/submit.rs index f41697ac6b..569d33a152 100644 --- a/crates/core/app/src/action_handler/actions/submit.rs +++ b/crates/core/app/src/action_handler/actions/submit.rs @@ -14,7 +14,6 @@ use penumbra_proto::DomainType; use penumbra_sct::component::StateReadExt as _; use penumbra_shielded_pool::component::SupplyWrite; use penumbra_storage::{StateDelta, StateRead, StateWrite}; -use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use penumbra_transaction::plan::TransactionPlan; use penumbra_transaction::Transaction; @@ -324,31 +323,20 @@ static DAO_FULL_VIEWING_KEY: Lazy = Lazy::new(|| { FullViewingKey::from_components(ak, nk) }); -/// The seed used for the random number generator used when constructing transactions made by the -/// DAO. -/// -/// It is arbitary, but must be deterministic in order to ensure that every node in the network -/// constructs a byte-for-byte identical transaction. -const DAO_TRANSACTION_RNG_SEED: &[u8; 32] = b"Penumbra DAO's tx build rng seed"; - async fn build_dao_transaction(transaction_plan: TransactionPlan) -> Result { let effect_hash = transaction_plan.effect_hash(&DAO_FULL_VIEWING_KEY); - transaction_plan - .build( - &DAO_FULL_VIEWING_KEY, - WitnessData { - anchor: penumbra_tct::Tree::new().root(), - state_commitment_proofs: Default::default(), - }, - )? - .authorize( - &mut ChaCha20Rng::from_seed(*DAO_TRANSACTION_RNG_SEED), - &AuthorizationData { - effect_hash, - spend_auths: Default::default(), - delegator_vote_auths: Default::default(), - }, - ) + transaction_plan.build( + &DAO_FULL_VIEWING_KEY, + &WitnessData { + anchor: penumbra_tct::Tree::new().root(), + state_commitment_proofs: Default::default(), + }, + &AuthorizationData { + effect_hash, + spend_auths: Default::default(), + delegator_vote_auths: Default::default(), + }, + ) } #[cfg(test)] diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index b438da9aff..67a0acc2eb 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -177,13 +177,10 @@ mod tests { }) .collect(), }; - let mut rng = OsRng; let tx = plan - .build_concurrent(&mut rng, fvk, witness_data) + .build_concurrent(fvk, &witness_data, &auth_data) .await - .expect("can build transaction") - .authorize(&mut rng, &auth_data) - .expect("can authorize transaction"); + .expect("can build transaction"); let context = tx.context(); @@ -241,13 +238,10 @@ mod tests { }) .collect(), }; - let mut rng = OsRng; let mut tx = plan - .build_concurrent(&mut rng, fvk, witness_data) + .build_concurrent(fvk, &witness_data, &auth_data) .await - .expect("can build transaction") - .authorize(&mut rng, &auth_data) - .expect("can authorize transaction"); + .expect("can build transaction"); // Set the anchor to the wrong root. tx.anchor = wrong_root; diff --git a/crates/core/component/dex/src/lp/plan.rs b/crates/core/component/dex/src/lp/plan.rs index bb35aa3208..922ebac3b9 100644 --- a/crates/core/component/dex/src/lp/plan.rs +++ b/crates/core/component/dex/src/lp/plan.rs @@ -127,6 +127,7 @@ impl PositionRewardClaimPlan { todo!() } + // FIXME: why is there an auth sig here?? /// Convenience method to construct the [`PositionRewardClaim`] described by this [`PositionRewardClaimPlan`]. pub fn position_reward_claim( &self, diff --git a/crates/core/keys/src/symmetric.rs b/crates/core/keys/src/symmetric.rs index 32799d60ea..ccadba221e 100644 --- a/crates/core/keys/src/symmetric.rs +++ b/crates/core/keys/src/symmetric.rs @@ -50,7 +50,7 @@ impl PayloadKind { /// Represents a symmetric `ChaCha20Poly1305` key. /// /// Used for encrypting and decrypting notes, swaps, memos, and memo keys. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct PayloadKey(Key); impl PayloadKey { diff --git a/crates/core/transaction/Cargo.toml b/crates/core/transaction/Cargo.toml index 53fd20a656..bc648c17f6 100644 --- a/crates/core/transaction/Cargo.toml +++ b/crates/core/transaction/Cargo.toml @@ -57,6 +57,7 @@ serde_json = "1" tracing = "0.1" tokio = { version = "1.21.1", features = ["full"], optional = true } clap = { version = "3", features = ["derive"], optional = true } +wasm-bindgen-test = "0.3.37" [dev-dependencies] proptest = "1" diff --git a/crates/core/transaction/src/action.rs b/crates/core/transaction/src/action.rs index 515f6f1489..07dad85fdc 100644 --- a/crates/core/transaction/src/action.rs +++ b/crates/core/transaction/src/action.rs @@ -5,9 +5,11 @@ use penumbra_asset::balance; use penumbra_proto::{core::transaction::v1alpha1 as pb, DomainType, TypeUrl}; use crate::{ActionView, IsAction, TransactionPerspective}; +use serde::{Deserialize, Serialize}; /// An action performed by a Penumbra transaction. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(try_from = "pb::Action", into = "pb::Action")] #[allow(clippy::large_enum_variant)] pub enum Action { Output(penumbra_shielded_pool::Output), diff --git a/crates/core/transaction/src/effect_hash.rs b/crates/core/transaction/src/effect_hash.rs index 49a8da4bd6..0fe532d2ac 100644 --- a/crates/core/transaction/src/effect_hash.rs +++ b/crates/core/transaction/src/effect_hash.rs @@ -1,4 +1,3 @@ -use blake2b_simd::Params; use decaf377_fmd::Clue; use penumbra_chain::EffectHash; use penumbra_dao::{DaoDeposit, DaoOutput, DaoSpend}; @@ -11,16 +10,17 @@ use penumbra_governance::{ DelegatorVote, DelegatorVoteBody, Proposal, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, ValidatorVoteBody, Vote, }; +use penumbra_ibc::IbcRelay; use penumbra_keys::{FullViewingKey, PayloadKey}; +use penumbra_proto::TypeUrl; use penumbra_proto::{ core::component::dex::v1alpha1 as pbd, core::component::fee::v1alpha1 as pbf, core::component::governance::v1alpha1 as pbg, core::component::ibc::v1alpha1 as pbi, core::component::shielded_pool::v1alpha1 as pb_sp, core::component::stake::v1alpha1 as pbs, core::transaction::v1alpha1 as pbt, crypto::decaf377_fmd::v1alpha1 as pb_fmd, Message, }; -use penumbra_proto::{DomainType, TypeUrl}; use penumbra_shielded_pool::{output, spend, Ics20Withdrawal}; -use penumbra_stake::{Delegate, Undelegate, UndelegateClaimBody}; +use penumbra_stake::{validator, Delegate, Undelegate, UndelegateClaimBody}; use crate::{ memo::MemoCiphertext, plan::TransactionPlan, transaction::DetectionData, Action, Transaction, @@ -139,101 +139,19 @@ impl TransactionPlan { let num_actions = self.actions.len() as u32; state.update(&num_actions.to_le_bytes()); - // TransactionPlan::build builds the actions sorted by type, so hash the - // actions in the order they'll appear in the final transaction. - for spend in self.spend_plans() { - state.update(spend.spend_body(fvk).effect_hash().as_bytes()); - } - // If the memo_key is None, then there is no memo, and we populate the memo key // field with a dummy key. let dummy_payload_key: PayloadKey = [0u8; 32].into(); - for output in self.output_plans() { - state.update( - output - .output_body( - fvk.outgoing(), - memo_key.as_ref().unwrap_or(&dummy_payload_key), - ) - .effect_hash() - .as_bytes(), - ); - } - for swap in self.swap_plans() { - state.update(swap.swap_body(fvk).effect_hash().as_bytes()); - } - for swap_claim in self.swap_claim_plans() { - state.update(swap_claim.swap_claim_body(fvk).effect_hash().as_bytes()); - } - for delegation in self.delegations() { - state.update(delegation.effect_hash().as_bytes()); - } - for undelegation in self.undelegations() { - state.update(undelegation.effect_hash().as_bytes()); - } - for plan in self.undelegate_claim_plans() { - state.update(plan.undelegate_claim_body().effect_hash().as_bytes()); - } - for proposal_submit in self.proposal_submits() { - state.update(proposal_submit.effect_hash().as_bytes()); - } - for proposal_withdraw in self.proposal_withdraws() { - state.update(proposal_withdraw.effect_hash().as_bytes()); - } - for validator_vote in self.validator_votes() { - state.update(validator_vote.effect_hash().as_bytes()); - } - for delegator_vote in self.delegator_vote_plans() { - state.update( - delegator_vote - .delegator_vote_body(fvk) - .effect_hash() - .as_bytes(), - ); - } - for proposal_deposit_claim in self.proposal_deposit_claims() { - state.update(proposal_deposit_claim.effect_hash().as_bytes()); - } - // These are data payloads, so just hash them directly, - // since they are effecting data. - for payload in self.validator_definitions() { - let effect_hash = Params::default() - .personal(b"PAH:valdefnition") - .hash(&payload.encode_to_vec()); - state.update(effect_hash.as_bytes()); - } - for payload in self.ibc_actions() { - let effect_hash = Params::default() - .personal(b"PAH:ibc_action") - .hash(&payload.encode_to_vec()); - state.update(effect_hash.as_bytes()); - } - for dao_spend in self.dao_spends() { - state.update(dao_spend.effect_hash().as_bytes()); - } - for dao_output in self.dao_outputs() { - state.update(dao_output.effect_hash().as_bytes()); - } - for dao_deposit in self.dao_deposits() { - state.update(dao_deposit.effect_hash().as_bytes()); - } - for position_open in self.position_openings() { - state.update(position_open.effect_hash().as_bytes()); - } - for position_close in self.position_closings() { - state.update(position_close.effect_hash().as_bytes()); - } - for position_withdraw in self.position_withdrawals() { + + // Hash the effecting data of each action, in the order it appears in the plan, + // which will be the order it appears in the transaction. + for action_plan in &self.actions { state.update( - position_withdraw - .position_withdraw() - .effect_hash() + action_plan + .effect_hash(fvk, memo_key.as_ref().unwrap_or(&dummy_payload_key)) .as_bytes(), ); } - for ics20_withdrawal in self.ics20_withdrawals() { - state.update(ics20_withdrawal.effect_hash().as_bytes()); - } EffectHash(state.finalize().as_array().clone()) } @@ -254,22 +172,8 @@ impl EffectingData for Action { Action::ValidatorVote(vote) => vote.effect_hash(), Action::SwapClaim(swap_claim) => swap_claim.body.effect_hash(), Action::Swap(swap) => swap.body.effect_hash(), - // These are data payloads, so just hash them directly, - // since we consider them authorizing data. - Action::ValidatorDefinition(payload) => EffectHash( - Params::default() - .personal(b"PAH:valdefnition") - .hash(&payload.encode_to_vec()) - .as_array() - .clone(), - ), - Action::IbcRelay(payload) => EffectHash( - Params::default() - .personal(b"PAH:ibc_action") - .hash(&payload.encode_to_vec()) - .as_array() - .clone(), - ), + Action::ValidatorDefinition(defn) => defn.effect_hash(), + Action::IbcRelay(payload) => payload.effect_hash(), Action::PositionOpen(p) => p.effect_hash(), Action::PositionClose(p) => p.effect_hash(), Action::PositionWithdraw(p) => p.effect_hash(), @@ -305,6 +209,20 @@ fn create_personalized_state(personalization: &str) -> blake2b_simd::State { state } +impl EffectingData for validator::Definition { + fn effect_hash(&self) -> EffectHash { + let effecting_data: pbs::ValidatorDefinition = self.clone().into(); + hash_proto_effecting_data(validator::Definition::TYPE_URL, &effecting_data) + } +} + +impl EffectingData for IbcRelay { + fn effect_hash(&self) -> EffectHash { + let effecting_data: pbi::IbcRelay = self.clone().into(); + hash_proto_effecting_data(IbcRelay::TYPE_URL, &effecting_data) + } +} + impl EffectingData for Ics20Withdrawal { fn effect_hash(&self) -> EffectHash { let effecting_data: pbi::Ics20Withdrawal = self.clone().into(); @@ -648,11 +566,7 @@ mod tests { }) .collect(), }; - let transaction = plan - .build(fvk, witness_data) - .unwrap() - .authorize(&mut OsRng, &auth_data) - .unwrap(); + let transaction = plan.build(fvk, &witness_data, &auth_data).unwrap(); let transaction_effect_hash = transaction.effect_hash(); diff --git a/crates/core/transaction/src/plan.rs b/crates/core/transaction/src/plan.rs index c3a31ec7f4..8fd6ce2858 100644 --- a/crates/core/transaction/src/plan.rs +++ b/crates/core/transaction/src/plan.rs @@ -14,7 +14,7 @@ use penumbra_governance::{ DelegatorVotePlan, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, }; use penumbra_ibc::IbcRelay; -use penumbra_keys::Address; +use penumbra_keys::{Address, PayloadKey}; use penumbra_proto::{core::transaction::v1alpha1 as pb, DomainType, TypeUrl}; use penumbra_shielded_pool::{Ics20Withdrawal, OutputPlan, SpendPlan}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaimPlan}; @@ -289,6 +289,13 @@ impl TransactionPlan { self.clue_plans = clue_plans; } + + /// Convenience method to grab the `MemoKey` from the plan. + pub fn memo_key(&self) -> Option { + self.memo_plan + .as_ref() + .map(|memo_plan| memo_plan.key.clone()) + } } impl TypeUrl for TransactionPlan { diff --git a/crates/core/transaction/src/plan/action.rs b/crates/core/transaction/src/plan/action.rs index 8481dfc293..872d0d9d8e 100644 --- a/crates/core/transaction/src/plan/action.rs +++ b/crates/core/transaction/src/plan/action.rs @@ -1,6 +1,13 @@ -use anyhow::anyhow; +use crate::Action; +use crate::EffectingData; +use crate::WitnessData; +use anyhow::{anyhow, Context, Result}; +use ark_ff::Zero; +use decaf377::Fr; use penumbra_asset::Balance; +use penumbra_chain::EffectHash; use penumbra_dao::{DaoDeposit, DaoOutput, DaoSpend}; + use penumbra_dex::{ lp::{ action::{PositionClose, PositionOpen}, @@ -15,6 +22,7 @@ use penumbra_governance::{ }; use penumbra_ibc::IbcRelay; +use penumbra_keys::{symmetric::PayloadKey, FullViewingKey}; use penumbra_proto::{core::transaction::v1alpha1 as pb_t, DomainType, TypeUrl}; use penumbra_shielded_pool::{Ics20Withdrawal, OutputPlan, SpendPlan}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaimPlan}; @@ -74,6 +82,88 @@ pub enum ActionPlan { } impl ActionPlan { + /// Builds a planned [`Action`] specified by this [`ActionPlan`]. + /// + /// The resulting action is `unauth` in the sense that this method does not + /// have access to authorization data, so any required authorization data + /// will be filled in with dummy zero values, to be replaced later. + /// + /// This method is useful for controlling how a transaction's actions are + /// built (e.g., building them in parallel, or via Web Workers). + pub fn build_unauth( + &self, + fvk: &FullViewingKey, + witness_data: &WitnessData, + memo_key: Option, + ) -> Result { + use ActionPlan::*; + + Ok(match self { + Spend(spend_plan) => { + let note_commitment = spend_plan.note.commit(); + let auth_path = witness_data + .state_commitment_proofs + .get(¬e_commitment) + .context(format!("could not get proof for {note_commitment:?}"))?; + + Action::Spend(spend_plan.spend( + fvk, + [0; 64].into(), + auth_path.clone(), + // FIXME: why does this need the anchor? isn't that implied by the auth_path? + // cf. delegator_vote + witness_data.anchor, + )) + } + Output(output_plan) => { + let dummy_payload_key: PayloadKey = [0u8; 32].into(); + Action::Output(output_plan.output( + fvk.outgoing(), + memo_key.as_ref().unwrap_or(&dummy_payload_key), + )) + } + Swap(swap_plan) => Action::Swap(swap_plan.swap(fvk)), + SwapClaim(swap_claim_plan) => { + let note_commitment = swap_claim_plan.swap_plaintext.swap_commitment(); + let auth_path = witness_data + .state_commitment_proofs + .get(¬e_commitment) + .context(format!("could not get proof for {note_commitment:?}"))?; + + Action::SwapClaim(swap_claim_plan.swap_claim(&fvk, auth_path)) + } + Delegate(plan) => Action::Delegate(plan.clone()), + Undelegate(plan) => Action::Undelegate(plan.clone()), + UndelegateClaim(plan) => Action::UndelegateClaim(plan.undelegate_claim()), + ValidatorDefinition(plan) => Action::ValidatorDefinition(plan.clone()), + // Fixme: action name + IbcAction(plan) => Action::IbcRelay(plan.clone()), + ProposalSubmit(plan) => Action::ProposalSubmit(plan.clone()), + ProposalWithdraw(plan) => Action::ProposalWithdraw(plan.clone()), + DelegatorVote(plan) => { + let note_commitment = plan.staked_note.commit(); + let auth_path = witness_data + .state_commitment_proofs + .get(¬e_commitment) + .context(format!("could not get proof for {note_commitment:?}"))?; + Action::DelegatorVote(plan.delegator_vote(fvk, [0; 64].into(), auth_path.clone())) + } + ValidatorVote(plan) => Action::ValidatorVote(plan.clone()), + ProposalDepositClaim(plan) => Action::ProposalDepositClaim(plan.clone()), + PositionOpen(plan) => Action::PositionOpen(plan.clone()), + PositionClose(plan) => Action::PositionClose(plan.clone()), + PositionWithdraw(plan) => Action::PositionWithdraw(plan.position_withdraw()), + PositionRewardClaim(_plan) => unimplemented!( + "this api is wrong and needs to be fixed, but we don't do reward claims anyways" + ), + DaoSpend(plan) => Action::DaoSpend(plan.clone()), + DaoOutput(plan) => Action::DaoOutput(plan.clone()), + DaoDeposit(plan) => Action::DaoDeposit(plan.clone()), + // Fixme: action name + Withdrawal(plan) => Action::Ics20Withdrawal(plan.clone()), + }) + } + pub fn balance(&self) -> Balance { use ActionPlan::*; @@ -101,6 +191,65 @@ impl ActionPlan { IbcAction(_) | ValidatorDefinition(_) | ValidatorVote(_) => Balance::default(), } } + + pub fn value_blinding(&self) -> Fr { + use ActionPlan::*; + + match self { + Spend(spend) => spend.value_blinding, + Output(output) => output.value_blinding, + Delegate(_) => Fr::zero(), + Undelegate(_) => Fr::zero(), + UndelegateClaim(undelegate_claim) => undelegate_claim.balance_blinding, + ValidatorDefinition(_) => Fr::zero(), + Swap(swap) => swap.fee_blinding, + SwapClaim(_) => Fr::zero(), + IbcAction(_) => Fr::zero(), + ProposalSubmit(_) => Fr::zero(), + ProposalWithdraw(_) => Fr::zero(), + DelegatorVote(_) => Fr::zero(), + ValidatorVote(_) => Fr::zero(), + ProposalDepositClaim(_) => Fr::zero(), + PositionOpen(_) => Fr::zero(), + PositionClose(_) => Fr::zero(), + PositionWithdraw(_) => Fr::zero(), + PositionRewardClaim(_) => Fr::zero(), + DaoSpend(_) => Fr::zero(), + DaoOutput(_) => Fr::zero(), + DaoDeposit(_) => Fr::zero(), + Withdrawal(_) => Fr::zero(), + } + } + + /// Compute the effect hash of the action this plan will produce. + pub fn effect_hash(&self, fvk: &FullViewingKey, memo_key: &PayloadKey) -> EffectHash { + use ActionPlan::*; + + match self { + Spend(plan) => plan.spend_body(fvk).effect_hash(), + Output(plan) => plan.output_body(fvk.outgoing(), memo_key).effect_hash(), + Delegate(plan) => plan.effect_hash(), + Undelegate(plan) => plan.effect_hash(), + UndelegateClaim(plan) => plan.undelegate_claim_body().effect_hash(), + ValidatorDefinition(plan) => plan.effect_hash(), + Swap(plan) => plan.swap_body(fvk).effect_hash(), + SwapClaim(plan) => plan.swap_claim_body(fvk).effect_hash(), + IbcAction(plan) => plan.effect_hash(), + ProposalSubmit(plan) => plan.effect_hash(), + ProposalWithdraw(plan) => plan.effect_hash(), + DelegatorVote(plan) => plan.delegator_vote_body(fvk).effect_hash(), + ValidatorVote(plan) => plan.effect_hash(), + ProposalDepositClaim(plan) => plan.effect_hash(), + PositionOpen(plan) => plan.effect_hash(), + PositionClose(plan) => plan.effect_hash(), + PositionWithdraw(plan) => plan.position_withdraw().effect_hash(), + PositionRewardClaim(_plan) => todo!("position reward claim plan is not implemented"), + DaoSpend(plan) => plan.effect_hash(), + DaoOutput(plan) => plan.effect_hash(), + DaoDeposit(plan) => plan.effect_hash(), + Withdrawal(plan) => plan.effect_hash(), + } + } } // Convenience impls that make declarative transaction construction easier. diff --git a/crates/core/transaction/src/plan/build.rs b/crates/core/transaction/src/plan/build.rs index 205e876c0d..8ca5c43924 100644 --- a/crates/core/transaction/src/plan/build.rs +++ b/crates/core/transaction/src/plan/build.rs @@ -1,308 +1,34 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use ark_ff::Zero; use decaf377::Fr; use decaf377_rdsa as rdsa; -use penumbra_keys::{symmetric::PayloadKey, FullViewingKey}; +use penumbra_keys::FullViewingKey; +use rand_core::OsRng; use rand_core::{CryptoRng, RngCore}; +use std::fmt::Debug; use super::TransactionPlan; use crate::{ action::Action, - memo::MemoCiphertext, transaction::{DetectionData, TransactionParameters}, AuthorizationData, AuthorizingData, Transaction, TransactionBody, WitnessData, }; impl TransactionPlan { - /// Build the transaction this plan describes. - /// - /// To turn a transaction plan into an unauthorized transaction, we need: - /// - /// - `fvk`, the [`FullViewingKey`] for the source funds; - /// - `witness_data`, the [`WitnessData`] used for proving; - /// - pub fn build( - self, - fvk: &FullViewingKey, - witness_data: WitnessData, - ) -> Result { - let mut actions = Vec::new(); - let mut synthetic_blinding_factor = Fr::zero(); - - // Add the memo. - let mut memo: Option = None; - let mut memo_key: Option = None; - if self.memo_plan.is_some() { - let memo_plan = self - .memo_plan - .clone() - .ok_or_else(|| anyhow!("missing memo_plan in TransactionPlan"))?; - memo = memo_plan.memo().ok(); - memo_key = Some(memo_plan.key); - } - - // We build the actions sorted by type, with all spends first, then all - // outputs, etc. This order has to align with the ordering in - // TransactionPlan::effect_hash, which computes the auth hash of the - // transaction we'll build here without actually building it. - - // Build the transaction's spends. - for spend_plan in self.spend_plans() { - let note_commitment = spend_plan.note.commit(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))?; - - synthetic_blinding_factor += spend_plan.value_blinding; - actions.push(Action::Spend(spend_plan.spend( - fvk, - [0; 64].into(), - auth_path.clone(), - witness_data.anchor, - ))); - } - - // Build the transaction's outputs. - let dummy_payload_key: PayloadKey = [0u8; 32].into(); - // If the memo_key is None, then there is no memo, and we populate the memo key - // field with a dummy key. - for output_plan in self.output_plans() { - // Outputs subtract from the transaction's value balance. - synthetic_blinding_factor += output_plan.value_blinding; - actions.push(Action::Output(output_plan.output( - fvk.outgoing(), - memo_key.as_ref().unwrap_or(&dummy_payload_key), - ))); - } - - // Build the transaction's swaps. - for swap_plan in self.swap_plans() { - synthetic_blinding_factor += swap_plan.fee_blinding; - actions.push(Action::Swap(swap_plan.swap(fvk))); - } - - // Build the transaction's swap claims. - for swap_claim_plan in self.swap_claim_plans().cloned() { - let note_commitment = swap_claim_plan.swap_plaintext.swap_commitment(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))?; - - actions.push(Action::SwapClaim( - swap_claim_plan.swap_claim(fvk, auth_path), - )); - } - - // Add detection data when there are outputs. - let detection_data: Option = if self.num_outputs() == 0 { - None - } else { - let mut fmd_clues = Vec::new(); - for clue_plan in self.clue_plans() { - fmd_clues.push(clue_plan.clue()); - } - Some(DetectionData { fmd_clues }) - }; - - // All of these actions have "transparent" value balance with no - // blinding factor, so they don't contribute to the - // synthetic_blinding_factor used for the binding signature. - - for delegation in self.delegations().cloned() { - actions.push(Action::Delegate(delegation)) - } - for undelegation in self.undelegations().cloned() { - actions.push(Action::Undelegate(undelegation)) - } - for plan in self.undelegate_claim_plans() { - synthetic_blinding_factor += plan.balance_blinding; - let undelegate_claim = plan.undelegate_claim(); - actions.push(Action::UndelegateClaim(undelegate_claim)); - } - for proposal_submit in self.proposal_submits().cloned() { - actions.push(Action::ProposalSubmit(proposal_submit)) - } - for proposal_withdraw_plan in self.proposal_withdraws().cloned() { - actions.push(Action::ProposalWithdraw(proposal_withdraw_plan)); - } - for validator_vote in self.validator_votes().cloned() { - actions.push(Action::ValidatorVote(validator_vote)) - } - for delegator_vote_plan in self.delegator_vote_plans() { - let note_commitment = delegator_vote_plan.staked_note.commit(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))?; - - actions.push(Action::DelegatorVote(delegator_vote_plan.delegator_vote( - fvk, - [0; 64].into(), - auth_path.clone(), - ))); - } - for proposal_deposit_claim in self.proposal_deposit_claims().cloned() { - actions.push(Action::ProposalDepositClaim(proposal_deposit_claim)) - } - for vd in self.validator_definitions().cloned() { - actions.push(Action::ValidatorDefinition(vd)) - } - for ibc_action in self.ibc_actions().cloned() { - actions.push(Action::IbcRelay(ibc_action)) - } - for dao_spend in self.dao_spends().cloned() { - actions.push(Action::DaoSpend(dao_spend)) - } - for dao_output in self.dao_outputs().cloned() { - actions.push(Action::DaoOutput(dao_output)) - } - for dao_deposit in self.dao_deposits().cloned() { - actions.push(Action::DaoDeposit(dao_deposit)) - } - for position_open in self.position_openings().cloned() { - actions.push(Action::PositionOpen(position_open)) - } - for position_close in self.position_closings().cloned() { - actions.push(Action::PositionClose(position_close)) - } - for position_withdraw in self.position_withdrawals() { - actions.push(Action::PositionWithdraw( - position_withdraw.position_withdraw(), - )) - } - // build the transaction's ICS20 withdrawals - for ics20_withdrawal in self.ics20_withdrawals() { - actions.push(Action::Ics20Withdrawal(ics20_withdrawal.clone())) - } - - let transaction_body = TransactionBody { - actions, - transaction_parameters: TransactionParameters { - expiry_height: self.expiry_height, - chain_id: self.chain_id, - }, - fee: self.fee, - detection_data, - memo, - }; - - // TODO: add consistency checks? - - Ok(UnauthTransaction { - inner: Transaction { - transaction_body, - anchor: witness_data.anchor, - binding_sig: [0; 64].into(), - }, - synthetic_blinding_factor, - }) - } - - #[cfg(feature = "parallel")] - /// Build the transaction this plan describes while proving concurrently. - /// This can be used in environments that support tokio tasks. - pub async fn build_concurrent( + /// Builds a [`TransactionPlan`] by slotting in the + /// provided prebuilt actions instead of using the + /// [`ActionPlan`]s in the TransactionPlan. + pub fn build_unauth_with_actions( self, - rng: R, - fvk: &FullViewingKey, - witness_data: WitnessData, - ) -> Result { - let mut synthetic_blinding_factor = Fr::zero(); - - // Add the memo. - let mut memo: Option = None; - let mut memo_key: Option = None; - if self.memo_plan.is_some() { - let memo_plan = self - .memo_plan - .clone() - .ok_or_else(|| anyhow!("missing memo_plan in TransactionPlan"))?; - memo = memo_plan.memo().ok(); - memo_key = Some(memo_plan.key); - } - - // We build the actions sorted by type, with all spends first, then all - // outputs, etc. This order has to align with the ordering in - // TransactionPlan::effect_hash, which computes the auth hash of the - // transaction we'll build here without actually building it. - - // Start building the transaction's spends. - let mut in_progress_spend_actions = Vec::new(); - for spend_plan in self.spend_plans().cloned() { - let note_commitment = spend_plan.note.commit(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))? - .clone(); - - synthetic_blinding_factor += spend_plan.value_blinding; - let fvk_ = fvk.clone(); - in_progress_spend_actions.push(tokio::spawn(async move { - //Add dummy auth sig for UnauthTransaction - let auth_sig = [0; 64].into(); - spend_plan.spend(&fvk_, auth_sig, auth_path, witness_data.anchor) - })); - } - - // Start building the transaction's outputs. - let mut in_progress_output_actions = Vec::new(); - let dummy_payload_key: PayloadKey = [0u8; 32].into(); - // If the memo_key is None, then there is no memo, and we populate the memo key - // field with a dummy key. - for output_plan in self.output_plans().cloned() { - // Outputs subtract from the transaction's value balance. - synthetic_blinding_factor += output_plan.value_blinding; - let ovk = fvk.outgoing().clone(); - let memo_key = memo_key.as_ref().unwrap_or(&dummy_payload_key).clone(); - in_progress_output_actions.push(tokio::spawn(async move { - output_plan.output(&ovk, &memo_key) - })); - } - - // Start building the transaction's swaps. - let mut in_progress_swap_actions = Vec::new(); - for swap_plan in self.swap_plans().cloned() { - synthetic_blinding_factor += swap_plan.fee_blinding; - let fvk_ = fvk.clone(); - in_progress_swap_actions.push(tokio::spawn(async move { swap_plan.swap(&fvk_) })); - } - - // Start building the transaction's swap claims. - let mut in_progress_swap_claim_actions = Vec::new(); - for swap_claim_plan in self.swap_claim_plans().cloned() { - let note_commitment = swap_claim_plan.swap_plaintext.swap_commitment(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))? - .clone(); - let fvk_ = fvk.clone(); - - in_progress_swap_claim_actions.push(tokio::spawn(async move { - swap_claim_plan.swap_claim(&fvk_, &auth_path) - })); - } - - // Start building the transaction's delegator votes. - let mut in_progress_delegator_vote_actions = Vec::new(); - for delegator_vote_plan in self.delegator_vote_plans().cloned() { - let note_commitment = delegator_vote_plan.staked_note.commit(); - let auth_path = witness_data - .state_commitment_proofs - .get(¬e_commitment) - .context(format!("could not get proof for {note_commitment:?}"))? - .clone(); - let fvk_ = fvk.clone(); - - in_progress_delegator_vote_actions.push(tokio::spawn(async move { - //Add dummy auth sig for UnauthTransaction - let auth_sig = [0; 64].into(); - delegator_vote_plan.delegator_vote(&fvk_, auth_sig, auth_path.clone()) - })); - } + actions: Vec, + witness_data: &WitnessData, + ) -> Result { + // Add the memo if it is planned. + let memo = self + .memo_plan + .as_ref() + .map(|memo_plan| memo_plan.memo()) + .transpose()?; // Add detection data when there are outputs. let detection_data: Option = if self.num_outputs() == 0 { @@ -315,93 +41,6 @@ impl TransactionPlan { Some(DetectionData { fmd_clues }) }; - // Actions with ZK proofs are slow to build and were done concurrently, - // so we resolve the corresponding `JoinHandle`s in the order the tasks were started. - let mut actions = Vec::new(); - // Collect the spend actions. - for action in in_progress_spend_actions { - actions.push(Action::Spend(action.await.expect("can form spend action"))); - } - // Collect the output actions. - for action in in_progress_output_actions { - actions.push(Action::Output( - action.await.expect("can form output action"), - )); - } - // Collect the swap actions. - for action in in_progress_swap_actions { - actions.push(Action::Swap(action.await.expect("can form swap action"))); - } - // Collect the swap claim actions. - for action in in_progress_swap_claim_actions { - actions.push(Action::SwapClaim( - action.await.expect("can form swap claim action"), - )); - } - - // All of these actions have "transparent" value balance with no - // blinding factor, so they don't contribute to the - // synthetic_blinding_factor used for the binding signature. - for delegation in self.delegations().cloned() { - actions.push(Action::Delegate(delegation)) - } - for undelegation in self.undelegations().cloned() { - actions.push(Action::Undelegate(undelegation)) - } - for plan in self.undelegate_claim_plans() { - synthetic_blinding_factor += plan.balance_blinding; - let undelegate_claim = plan.undelegate_claim(); - actions.push(Action::UndelegateClaim(undelegate_claim)); - } - for proposal_submit in self.proposal_submits().cloned() { - actions.push(Action::ProposalSubmit(proposal_submit)) - } - for proposal_withdraw_plan in self.proposal_withdraws().cloned() { - actions.push(Action::ProposalWithdraw(proposal_withdraw_plan)); - } - for validator_vote in self.validator_votes().cloned() { - actions.push(Action::ValidatorVote(validator_vote)) - } - for delegator_vote in in_progress_delegator_vote_actions { - actions.push(Action::DelegatorVote( - delegator_vote - .await - .expect("can form delegator vote action"), - )); - } - for proposal_deposit_claim in self.proposal_deposit_claims().cloned() { - actions.push(Action::ProposalDepositClaim(proposal_deposit_claim)) - } - for vd in self.validator_definitions().cloned() { - actions.push(Action::ValidatorDefinition(vd)) - } - for ibc_action in self.ibc_actions().cloned() { - actions.push(Action::IbcRelay(ibc_action)) - } - for dao_spend in self.dao_spends().cloned() { - actions.push(Action::DaoSpend(dao_spend)) - } - for dao_output in self.dao_outputs().cloned() { - actions.push(Action::DaoOutput(dao_output)) - } - for dao_deposit in self.dao_deposits().cloned() { - actions.push(Action::DaoDeposit(dao_deposit)) - } - for position_open in self.position_openings().cloned() { - actions.push(Action::PositionOpen(position_open)) - } - for position_close in self.position_closings().cloned() { - actions.push(Action::PositionClose(position_close)) - } - for position_withdraw in self.position_withdrawals() { - actions.push(Action::PositionWithdraw( - position_withdraw.position_withdraw(), - )) - } - for ics20_withdrawal in self.ics20_withdrawals().cloned() { - actions.push(Action::Ics20Withdrawal(ics20_withdrawal)) - } - let transaction_body = TransactionBody { actions, transaction_parameters: TransactionParameters { @@ -413,37 +52,24 @@ impl TransactionPlan { memo, }; - // Finally, compute the binding signature and assemble the transaction. - let binding_signing_key = rdsa::SigningKey::from(synthetic_blinding_factor); - let auth_hash = transaction_body.auth_hash(); - let binding_sig = binding_signing_key.sign(rng, auth_hash.as_bytes()); - tracing::debug!(bvk = ?rdsa::VerificationKey::from(&binding_signing_key), ?auth_hash); - - Ok(UnauthTransaction { - inner: Transaction { - transaction_body, - binding_sig, - anchor: witness_data.anchor, - }, - synthetic_blinding_factor, + Ok(Transaction { + transaction_body, + anchor: witness_data.anchor, + binding_sig: [0; 64].into(), }) } -} -/// A partially-constructed transaction awaiting authorization data. -pub struct UnauthTransaction { - inner: Transaction, - synthetic_blinding_factor: Fr, -} - -impl UnauthTransaction { - pub fn authorize( - mut self, + /// Slot in the [`AuthorizationData`] and derive the synthetic + /// blinding factors needed to compute the binding signature + /// and assemble the transaction. + pub fn apply_auth_data( + &self, rng: &mut R, auth_data: &AuthorizationData, + mut transaction: Transaction, ) -> Result { // Do some basic input sanity-checking. - let spend_count = self.inner.spends().count(); + let spend_count = transaction.spends().count(); if auth_data.spend_auths.len() != spend_count { anyhow::bail!( "expected {} spend auths but got {}", @@ -451,10 +77,17 @@ impl UnauthTransaction { auth_data.spend_auths.len() ); } - // Overwrite the placeholder auth sigs with the real ones from `auth_data` - for (spend, auth_sig) in self - .inner + // Derive the synthetic blinding factors from `TransactionPlan`. + let mut synthetic_blinding_factor = Fr::zero(); + + // Accumulate the blinding factors. + for action_plan in &self.actions { + synthetic_blinding_factor += action_plan.value_blinding(); + } + + // Overwrite the placeholder authorization signatures with the real `AuthorizationData`. + for (spend, auth_sig) in transaction .transaction_body .actions .iter_mut() @@ -470,8 +103,7 @@ impl UnauthTransaction { spend.auth_sig = auth_sig; } - for (delegator_vote, auth_sig) in self - .inner + for (delegator_vote, auth_sig) in transaction .transaction_body .actions .iter_mut() @@ -488,13 +120,87 @@ impl UnauthTransaction { } // Compute the binding signature and assemble the transaction. - let binding_signing_key = rdsa::SigningKey::from(self.synthetic_blinding_factor); - let auth_hash = self.inner.transaction_body.auth_hash(); + let binding_signing_key = rdsa::SigningKey::from(synthetic_blinding_factor); + let auth_hash = transaction.transaction_body.auth_hash(); + let binding_sig = binding_signing_key.sign(rng, auth_hash.as_bytes()); tracing::debug!(bvk = ?rdsa::VerificationKey::from(&binding_signing_key), ?auth_hash); - self.inner.binding_sig = binding_sig; + transaction.binding_sig = binding_sig; + + Ok(transaction) + } + + /// Build the serial transaction this plan describes. + pub fn build( + self, + full_viewing_key: &FullViewingKey, + witness_data: &WitnessData, + auth_data: &AuthorizationData, + ) -> Result { + // 1. Build each action. + let actions = self + .actions + .iter() + .map(|action_plan| { + action_plan.build_unauth(full_viewing_key, witness_data, self.memo_key()) + }) + .collect::>>()?; + + // 2. Pass in the prebuilt actions to the build method. + let tx = self + .clone() + .build_unauth_with_actions(actions, witness_data)?; + + // 3. Slot in the authorization data with .apply_auth_data, + let tx = self.apply_auth_data(&mut OsRng, auth_data, tx)?; + + // 4. Return the completed transaction. + Ok(tx) + } + + #[cfg(feature = "parallel")] + /// Build the transaction this plan describes while proving concurrently. + /// This can be used in environments that support tokio tasks. + pub async fn build_concurrent( + self, + full_viewing_key: &FullViewingKey, + witness_data: &WitnessData, + auth_data: &AuthorizationData, + ) -> Result { + // Clone the witness data into an Arc so it can be shared between tasks. + let witness_data = std::sync::Arc::new(witness_data.clone()); + + // 1. Build each action (concurrently). + let action_handles = self + .actions + .iter() + .cloned() + .map(|action_plan| { + let fvk2 = full_viewing_key.clone(); + let witness_data2 = witness_data.clone(); // Arc + let memo_key2 = self.memo_key(); + tokio::task::spawn_blocking(move || { + action_plan.build_unauth(&fvk2, &*witness_data2, memo_key2) + }) + }) + .collect::>(); + + // 1.5. Collect all of the actions. + let mut actions = Vec::new(); + for handle in action_handles { + actions.push(handle.await??); + } + + // 2. Pass in the prebuilt actions to the build method. + let tx = self + .clone() + .build_unauth_with_actions(actions, &*witness_data)?; + + // 3. Slot in the authorization data with .apply_auth_data, + let tx = self.apply_auth_data(&mut OsRng, auth_data, tx)?; - Ok(self.inner) + // 4. Return the completed transaction. + Ok(tx) } } diff --git a/crates/view/src/service.rs b/crates/view/src/service.rs index 90f0b3c281..2b414bf92b 100644 --- a/crates/view/src/service.rs +++ b/crates/view/src/service.rs @@ -1412,10 +1412,8 @@ impl ViewProtocolService for ViewService { let transaction = Some( transaction_plan - .build(&fvk, witness_data) + .build(&fvk, &witness_data, &authorization_data) .map_err(|_| tonic::Status::failed_precondition("Error building transaction"))? - .authorize(&mut OsRng, &authorization_data) - .map_err(|_| tonic::Status::failed_precondition("Error authorizing transaction"))? .into(), ); diff --git a/crates/wallet/src/build.rs b/crates/wallet/src/build.rs index 6442559ff3..af6520be0b 100644 --- a/crates/wallet/src/build.rs +++ b/crates/wallet/src/build.rs @@ -3,19 +3,16 @@ use penumbra_custody::{AuthorizeRequest, CustodyClient}; use penumbra_keys::FullViewingKey; use penumbra_transaction::{plan::TransactionPlan, AuthorizationData, Transaction}; use penumbra_view::ViewClient; -use rand_core::{CryptoRng, RngCore}; -pub async fn build_transaction( +pub async fn build_transaction( fvk: &FullViewingKey, view: &mut V, custody: &mut C, - mut rng: R, plan: TransactionPlan, ) -> Result where V: ViewClient, C: CustodyClient, - R: RngCore + CryptoRng, { // Get the authorization data from the custody service... let auth_data: AuthorizationData = custody @@ -42,11 +39,9 @@ where #[cfg(feature = "parallel")] { let tx = plan - .build_concurrent(&mut rng, fvk, witness_data) + .build_concurrent(fvk, &witness_data, &auth_data) .await - .map_err(|_| tonic::Status::failed_precondition("Error building transaction"))? - .authorize(&mut rng, &auth_data) - .map_err(|_| tonic::Status::failed_precondition("Error authorizing transaction"))?; + .map_err(|_| tonic::Status::failed_precondition("Error building transaction"))?; Ok(tx) } diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index 5a0074cef5..f00f6901dd 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -43,8 +43,9 @@ serde-wasm-bindgen = "0.5.0" thiserror = "1.0" wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4.37" -wasm-bindgen-test = "0.3.0" +wasm-bindgen-test = "0.3.37" web-sys = { version = "0.3.64", features = ["console"] } +serde_json = "1.0.107" [dev-dependencies] wasm-bindgen-test = "0.3.37" diff --git a/crates/wasm/src/error.rs b/crates/wasm/src/error.rs index 5370f3e1a3..d222633c57 100644 --- a/crates/wasm/src/error.rs +++ b/crates/wasm/src/error.rs @@ -10,6 +10,7 @@ use web_sys::DomException; use penumbra_tct::error::{InsertBlockError, InsertEpochError, InsertError}; pub type WasmResult = Result; +pub type WasmOption = Option; #[derive(Error, Debug)] pub enum WasmError { diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index df830d2107..3f86d95c3a 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -1,15 +1,13 @@ #![allow(dead_code)] extern crate core; -pub use view_server::ViewServer; - pub mod error; pub mod keys; -mod note_record; -mod planner; -mod storage; -mod swap_record; -mod tx; -mod utils; -mod view_server; -mod wasm_planner; +pub mod note_record; +pub mod planner; +pub mod storage; +pub mod swap_record; +pub mod tx; +pub mod utils; +pub mod view_server; +pub mod wasm_planner; diff --git a/crates/wasm/src/storage.rs b/crates/wasm/src/storage.rs index e37915873d..0fb5f02eeb 100644 --- a/crates/wasm/src/storage.rs +++ b/crates/wasm/src/storage.rs @@ -1,18 +1,21 @@ -use indexed_db_futures::prelude::OpenDbRequest; -use indexed_db_futures::{IdbDatabase, IdbQuerySource}; -use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsValue; -use web_sys::IdbTransactionMode::Readwrite; - +use indexed_db_futures::{ + prelude::{IdbObjectStoreParameters, IdbOpenDbRequestLike, OpenDbRequest}, + IdbDatabase, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent, +}; use penumbra_asset::asset::{DenomMetadata, Id}; -use penumbra_proto::crypto::tct::v1alpha1::StateCommitment; -use penumbra_proto::view::v1alpha1::{NotesRequest, SwapRecord}; -use penumbra_proto::DomainType; +use penumbra_proto::{ + crypto::tct::v1alpha1::StateCommitment, + view::v1alpha1::{NotesRequest, SwapRecord}, + DomainType, +}; use penumbra_sct::Nullifier; use penumbra_shielded_pool::{note, Note}; +use serde::{Deserialize, Serialize}; +use web_sys::IdbTransactionMode::Readwrite; use crate::error::{WasmError, WasmResult}; use crate::note_record::SpendableNoteRecord; +use wasm_bindgen::JsValue; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct IndexedDbConstants { @@ -36,11 +39,52 @@ pub struct IndexedDBStorage { impl IndexedDBStorage { pub async fn new(constants: IndexedDbConstants) -> WasmResult { - let db_req: OpenDbRequest = IdbDatabase::open_u32(&constants.name, constants.version)?; + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&constants.name, constants.version)?; + + // Conditionally create object stores in the `IdbDatabase` database for testing purposes + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + // Check if the object store exists; create it if it doesn't + if evt.db().name() == "penumbra-db-wasm-test" { + let note_key: JsValue = serde_wasm_bindgen::to_value("noteCommitment.inner")?; + let note_object_store_params = IdbObjectStoreParameters::new() + .key_path(Some(&IdbKeyPath::new(note_key))) + .to_owned(); + let note_object_store = evt.db().create_object_store_with_params( + "SPENDABLE_NOTES", + ¬e_object_store_params, + )?; + + let nullifier_key: JsValue = serde_wasm_bindgen::to_value("nullifier.inner")?; + note_object_store.create_index_with_params( + "nullifier", + &IdbKeyPath::new(nullifier_key), + web_sys::IdbIndexParameters::new().unique(false), + )?; + evt.db().create_object_store("TREE_LAST_POSITION")?; + evt.db().create_object_store("TREE_LAST_FORGOTTEN")?; + + let commitment_key: JsValue = serde_wasm_bindgen::to_value("commitment.inner")?; + let commitment_object_store_params = IdbObjectStoreParameters::new() + .key_path(Some(&IdbKeyPath::new(commitment_key))) + .to_owned(); + evt.db().create_object_store_with_params( + "TREE_COMMITMENTS", + &commitment_object_store_params, + )?; + evt.db().create_object_store("TREE_HASHES")?; + } + Ok(()) + })); + let db: IdbDatabase = db_req.into_future().await?; + Ok(IndexedDBStorage { db, constants }) } + pub fn get_database(&self) -> *const IdbDatabase { + &self.db + } + pub async fn get_notes(&self, request: NotesRequest) -> WasmResult> { let idb_tx = self .db diff --git a/crates/wasm/src/tx.rs b/crates/wasm/src/tx.rs index b4fe8c1054..b5a71a8c3f 100644 --- a/crates/wasm/src/tx.rs +++ b/crates/wasm/src/tx.rs @@ -22,6 +22,7 @@ use crate::storage::IndexedDBStorage; use crate::storage::IndexedDbConstants; use crate::utils; use crate::view_server::{load_tree, StoredTree}; +use penumbra_transaction::Action; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TxInfoResponse { @@ -145,7 +146,7 @@ pub fn witness(transaction_plan: JsValue, stored_tree: JsValue) -> WasmResult` +/// transaction_plan: `pb::TransactionPlan` +/// witness_data: `pb::WitnessData` +/// auth_data: `pb::AuthorizationData` +/// Returns: `pb::Transaction` +#[wasm_bindgen] +pub fn build_parallel( + actions: JsValue, + transaction_plan: JsValue, + witness_data: JsValue, + auth_data: JsValue, +) -> WasmResult { + utils::set_panic_hook(); + + let plan_proto: pb::TransactionPlan = serde_wasm_bindgen::from_value(transaction_plan)?; + let plan: TransactionPlan = plan_proto.try_into()?; + + let witness_data_proto: pb::WitnessData = serde_wasm_bindgen::from_value(witness_data)?; + let witness_data_: WitnessData = witness_data_proto.try_into()?; + + let auth_data_proto: pb::AuthorizationData = serde_wasm_bindgen::from_value(auth_data)?; + let auth_data_: AuthorizationData = auth_data_proto.try_into()?; + + let actions_: Vec = serde_wasm_bindgen::from_value(actions)?; + + let transaction = plan + .clone() + .build_unauth_with_actions(actions_, &witness_data_)?; + + let tx = plan.apply_auth_data(&mut OsRng, &auth_data_, transaction)?; + + let value = serde_wasm_bindgen::to_value(&tx.to_proto())?; + Ok(value) } @@ -221,7 +265,6 @@ pub async fn transaction_info_inner( // Next, extend the TxP with the openings of commitments known to our view server // but not included in the transaction body, for instance spent notes or swap claim outputs. for action in tx.actions() { - use penumbra_transaction::Action; match action { Action::Spend(spend) => { let nullifier = spend.body.nullifier; diff --git a/crates/wasm/src/wasm_planner.rs b/crates/wasm/src/wasm_planner.rs index 2aebcfdd25..a39ea9fa56 100644 --- a/crates/wasm/src/wasm_planner.rs +++ b/crates/wasm/src/wasm_planner.rs @@ -1,24 +1,33 @@ -use ark_ff::UniformRand; -use decaf377::Fq; -use rand_core::OsRng; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsValue; - -use penumbra_chain::params::{ChainParameters, FmdParameters}; -use penumbra_dex::swap_claim::SwapClaimPlan; -use penumbra_proto::core::asset::v1alpha1::{DenomMetadata, Value}; -use penumbra_proto::core::component::fee::v1alpha1::{Fee, GasPrices}; -use penumbra_proto::core::component::ibc::v1alpha1::Ics20Withdrawal; -use penumbra_proto::core::keys::v1alpha1::{Address, AddressIndex}; -use penumbra_proto::core::transaction::v1alpha1::MemoPlaintext; -use penumbra_proto::crypto::tct::v1alpha1::StateCommitment; -use penumbra_proto::DomainType; - use crate::error::WasmResult; use crate::planner::Planner; use crate::storage::IndexedDBStorage; use crate::swap_record::SwapRecord; use crate::utils; +use anyhow::{anyhow, Context}; +use ark_ff::UniformRand; +use decaf377::Fq; +use penumbra_chain::params::{ChainParameters, FmdParameters}; +use penumbra_dex::swap_claim::SwapClaimPlan; + +use penumbra_keys::{symmetric::PayloadKey, FullViewingKey}; +use penumbra_proto::{ + core::{ + asset::v1alpha1::{DenomMetadata, Value}, + component::fee::v1alpha1::{Fee, GasPrices}, + component::ibc::v1alpha1::Ics20Withdrawal, + keys::v1alpha1::{Address, AddressIndex}, + transaction::v1alpha1 as pb, + transaction::v1alpha1::MemoPlaintext, + transaction::v1alpha1::TransactionPlan as tp, + }, + crypto::tct::v1alpha1::StateCommitment, + DomainType, +}; +use penumbra_transaction::{action::Action, plan::ActionPlan, plan::TransactionPlan, WitnessData}; +use rand_core::OsRng; +use std::str::FromStr; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; #[wasm_bindgen] pub struct WasmPlanner { @@ -55,6 +64,141 @@ impl WasmPlanner { Ok(planner) } + /// Builds a planned [`Action`] specified by + /// the [`ActionPlan`] in a [`TransactionPlan`]. + /// Arguments: + /// &self: `WasmPlanner` + /// transaction_plan: `TransactionPlan` + /// action_plan: `ActionPlan` + /// full_viewing_key: `bech32m String`, + /// witness_data: `WitnessData`` + /// Returns: `Action` + #[wasm_bindgen] + pub fn build_action( + &self, + transaction_plan: JsValue, + action_plan: JsValue, + full_viewing_key: &str, + witness_data: JsValue, + ) -> WasmResult { + utils::set_panic_hook(); + + let transaction_plan_proto: tp = serde_wasm_bindgen::from_value(transaction_plan.clone())?; + let transaction_plan_: TransactionPlan = transaction_plan_proto.try_into()?; + + let witness_data_proto: pb::WitnessData = serde_wasm_bindgen::from_value(witness_data)?; + let witness_data_: WitnessData = witness_data_proto.try_into()?; + + let action_proto: pb::ActionPlan = serde_wasm_bindgen::from_value(action_plan)?; + let action_plan_: ActionPlan = action_proto.try_into()?; + + let full_viewing_key: FullViewingKey = FullViewingKey::from_str(full_viewing_key) + .expect("The provided string is not a valid FullViewingKey"); + + let mut memo_key: Option = None; + if transaction_plan_.memo_plan.is_some() { + let memo_plan = transaction_plan_ + .memo_plan + .clone() + .ok_or_else(|| anyhow!("missing memo_plan in TransactionPlan"))?; + memo_key = Some(memo_plan.key); + } + + let action = match action_plan_ { + ActionPlan::Spend(spend_plan) => { + let spend = ActionPlan::Spend(spend_plan); + Some( + spend + .build_unauth(&full_viewing_key, &witness_data_, memo_key) + .expect("Build spend action failed!"), + ) + } + ActionPlan::Output(output_plan) => { + let output = ActionPlan::Output(output_plan); + Some( + output + .build_unauth(&full_viewing_key, &witness_data_, memo_key) + .expect("Build output action failed!"), + ) + } + + // TODO: Other action variants besides 'Spend' and 'Output' still require testing. + ActionPlan::Swap(swap_plan) => { + let swap = ActionPlan::Swap(swap_plan); + Some( + swap.build_unauth(&full_viewing_key, &witness_data_, memo_key) + .expect("Build swap action failed!"), + ) + } + ActionPlan::SwapClaim(swap_claim_plan) => { + let swap_claim = ActionPlan::SwapClaim(swap_claim_plan); + Some( + swap_claim + .build_unauth(&full_viewing_key, &witness_data_, memo_key) + .expect("Build swap claim action failed!"), + ) + } + ActionPlan::Delegate(delegation) => Some(Action::Delegate(delegation)), + ActionPlan::Undelegate(undelegation) => Some(Action::Undelegate(undelegation)), + ActionPlan::UndelegateClaim(undelegate_claim) => { + let undelegate_claim = undelegate_claim.undelegate_claim(); + Some(Action::UndelegateClaim(undelegate_claim)) + } + ActionPlan::ProposalSubmit(proposal_submit) => { + Some(Action::ProposalSubmit(proposal_submit)) + } + ActionPlan::ProposalWithdraw(proposal_withdraw) => { + Some(Action::ProposalWithdraw(proposal_withdraw)) + } + ActionPlan::ValidatorVote(validator_vote) => { + Some(Action::ValidatorVote(validator_vote)) + } + ActionPlan::DelegatorVote(delegator_vote) => { + let note_commitment = delegator_vote.staked_note.commit(); + let auth_path = witness_data_ + .state_commitment_proofs + .get(¬e_commitment) + .context(format!("could not get proof for {note_commitment:?}"))?; + + Some(Action::DelegatorVote(delegator_vote.delegator_vote( + &full_viewing_key, + [0; 64].into(), + auth_path.clone(), + ))) + } + ActionPlan::ProposalDepositClaim(proposal_deposit_claim) => { + Some(Action::ProposalDepositClaim(proposal_deposit_claim)) + } + ActionPlan::ValidatorDefinition(validator_definition) => { + Some(Action::ValidatorDefinition(validator_definition)) + } + ActionPlan::IbcAction(ibc_action) => Some(Action::IbcRelay(ibc_action)), + ActionPlan::DaoSpend(dao_spend) => Some(Action::DaoSpend(dao_spend)), + ActionPlan::DaoOutput(dao_output) => Some(Action::DaoOutput(dao_output)), + ActionPlan::DaoDeposit(dao_deposit) => Some(Action::DaoDeposit(dao_deposit)), + ActionPlan::PositionOpen(position_open) => Some(Action::PositionOpen(position_open)), + ActionPlan::PositionClose(position_close) => { + Some(Action::PositionClose(position_close)) + } + ActionPlan::PositionWithdraw(position_withdrawn) => Some(Action::PositionWithdraw( + position_withdrawn.position_withdraw(), + )), + ActionPlan::Withdrawal(ics20_withdrawal) => { + Some(Action::Ics20Withdrawal(ics20_withdrawal)) + } + // TODO: Should we handle `PositionRewardClaim`? + _ => None, + }; + + let action_result_proto = serde_wasm_bindgen::to_value(&Some(action))?; + Ok(action_result_proto) + } + + /// Public getter for the 'storage' field + pub fn get_storage(&self) -> *const IndexedDBStorage { + &self.storage + } + /// Add expiry height to plan /// Arguments: /// expiry_height: `u64` @@ -80,7 +224,6 @@ impl WasmPlanner { /// memo: `MemoPlaintext` pub fn memo(&mut self, memo: JsValue) -> WasmResult<()> { utils::set_panic_hook(); - let memo_proto: MemoPlaintext = serde_wasm_bindgen::from_value(memo)?; let _ = self.planner.memo(memo_proto.try_into()?); Ok(()) diff --git a/crates/wasm/tests/test_build.rs b/crates/wasm/tests/test_build.rs new file mode 100644 index 0000000000..720ff60f2e --- /dev/null +++ b/crates/wasm/tests/test_build.rs @@ -0,0 +1,466 @@ +extern crate penumbra_wasm; + +#[cfg(test)] +mod tests { + use anyhow::Result; + use serde::{Deserialize, Serialize}; + use serde_json; + use wasm_bindgen::JsValue; + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + use indexed_db_futures::prelude::{ + IdbDatabase, IdbObjectStore, IdbQuerySource, IdbTransaction, IdbTransactionMode, + }; + + use penumbra_proto::{ + core::{ + asset::v1alpha1::Value, + component::chain::v1alpha1::{ChainParameters, FmdParameters}, + keys::v1alpha1::{Address, AddressIndex}, + transaction::v1alpha1::{MemoPlaintext, TransactionPlan as tp}, + }, + view::v1alpha1::SpendableNoteRecord, + }; + use penumbra_tct::{structure::Hash, Forgotten}; + use penumbra_transaction::{ + plan::{ActionPlan, TransactionPlan}, + Action, + }; + use penumbra_wasm::{ + error::WasmError, + storage::IndexedDBStorage, + tx::{authorize, build, build_parallel, witness}, + wasm_planner::WasmPlanner, + }; + + #[wasm_bindgen_test] + async fn mock_build_serial_and_parallel() { + // Limit the use of Penumbra Rust libraries since we're mocking JS calls + // that are based on constructing objects according to protobuf definitions. + + // Define database parameters + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct IndexedDbConstants { + name: String, + version: u32, + tables: Tables, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct Tables { + assets: String, + notes: String, + spendable_notes: String, + swaps: String, + } + + // IndexDB tables and constants. + let tables: Tables = Tables { + assets: "ASSETS".to_string(), + notes: "NOTES".to_string(), + spendable_notes: "SPENDABLE_NOTES".to_string(), + swaps: "SWAPS".to_string(), + }; + + let constants: IndexedDbConstants = IndexedDbConstants { + name: "penumbra-db-wasm-test".to_string(), + version: 1, + tables, + }; + + // Sample chain and fmd parameters. + let chain_params = ChainParameters { + chain_id: "penumbra-testnet-iapetus".to_string(), + epoch_duration: 5u64, + }; + + let fmd_params = FmdParameters { + precision_bits: 0u32, + as_of_block_height: 1u64, + }; + + // Serialize the parameters into `JsValue`. + let js_chain_params_value: JsValue = serde_wasm_bindgen::to_value(&chain_params).unwrap(); + let js_fmd_params_value: JsValue = serde_wasm_bindgen::to_value(&fmd_params).unwrap(); + let js_constants_params_value: JsValue = serde_wasm_bindgen::to_value(&constants).unwrap(); + + // Construct `WasmPlanner` instance. + let mut wasm_planner = WasmPlanner::new( + js_constants_params_value, + js_chain_params_value, + js_fmd_params_value, + ) + .await + .unwrap(); + + // Create spendable UTXO note in JSON format. + let spendable_note_json = r#" + { + "note_commitment": { + "inner": "MY7PmcrH4fhjFOoMIKEdF+x9EUhZ9CS/CIfVco7Y5wU=" + }, + "note": { + "value": { + "amount": { + "lo": "1000000", + "hi": "0" + }, + "asset_id": { + "inner": "nwPDkQq3OvLnBwGTD+nmv1Ifb2GEmFCgNHrU++9BsRE=", + "alt_bech32m": "", + "alt_base_denom": "" + } + }, + "rseed": "p2w4O1ognDJtKVqhHK2qsUbV+1AEM/gn58uWYQ5v3sM=", + "address": { + "inner": "F6T1P51M1QOu8NGhKTMdJTy72TDhB2h00uvlIUcXVdovybq4ZcOwROB+1VE/ar4thEDNPanAcaYOrL+FugN8e19pvr93ZqmTjUdOLic+w+U=", + "alt_bech32m": "" + } + }, + "address_index": { + "account": "0", + "randomizer": "AAAAAAAAAAAAAAAA" + }, + "nullifier": { + "inner": "8TvyFVKk16PHcOEAgl0QV4/92xdVpLdXI+zP87lBrQ8=" + }, + "height_created": "250305", + "height_spent": "0", + "position": "3204061134848", + "source": { + "inner": "oJ9Bo9v22srtUmKdTAMVwPOuGumWE2cAuBbZHci8B1I=" + } + } + "#; + + // Convert note to `SpendableNoteRecord`. + let spendable_note: SpendableNoteRecord = + serde_json::from_str(spendable_note_json).unwrap(); + + // Define neccessary parameters to mock `TransactionPlannerRequest` in JSON format. + let address_json = r#" + { + "alt_bech32m": "penumbra1dugkjttfezh4gfkqs77377gnjlvmkkehusx6953udxeescc0qpgk6gqc0jmrsjq8xphzrg938843p0e63z09vt8lzzmef0q330e5njuwh4290n8pemcmx70sasym0lcjkstgzc", + "inner": "" + } + "#; + let value_json = r#" + { + "amount": { + "lo": "1", + "hi": "0" + }, + "asset_id": { + "inner": "nwPDkQq3OvLnBwGTD+nmv1Ifb2GEmFCgNHrU++9BsRE=", + "alt_bech32m": "", + "alt_base_denom": "" + } + } + "#; + + // Convert fields to JsValue. + let address: Address = serde_json::from_str(address_json).unwrap(); + let value: Value = serde_json::from_str(value_json).unwrap(); + + // Add output action to plan. + wasm_planner + .output( + serde_wasm_bindgen::to_value(&value).unwrap(), + serde_wasm_bindgen::to_value(&address).unwrap(), + ) + .unwrap(); + + // Add memo to plan. + let memo: MemoPlaintext = MemoPlaintext { + return_address: Some(address), + text: "sample memo".to_string(), + }; + let memo_plan_deserialized = serde_wasm_bindgen::to_value(&memo).unwrap(); + wasm_planner.memo(memo_plan_deserialized).unwrap(); + + // Retrieve private database handle with public getters. + let storage = wasm_planner.get_storage(); + let storage_ref: &IndexedDBStorage = unsafe { &*storage }; + let database: *const IdbDatabase = storage_ref.get_database(); + let database_ref: &IdbDatabase = unsafe { &*database }; + + // Define SCT-related structs. + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct Position { + epoch: u64, + block: u64, + commitment: u64, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + #[allow(non_snake_case)] + pub struct StoredPosition { + Position: Position, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct StoreHash { + position: Position, + height: u64, + hash: Hash, + essential: bool, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct StoreCommitment { + commitment: Commitment, + position: Position, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct Commitment { + inner: String, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct StateCommitmentTree { + last_position: Position, + last_forgotten: u64, + hashes: StoreHash, + commitments: StoreCommitment, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct SctUpdates { + store_commitments: StoreCommitment, + set_position: StoredPosition, + set_forgotten: u64, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct StoredTree { + last_position: Option, + last_forgotten: Option, + hashes: Vec, + commitments: Vec, + } + + // Define a sample SCT update. + #[allow(non_snake_case)] + let sctUpdates = SctUpdates { + store_commitments: StoreCommitment { + commitment: Commitment { + inner: "MY7PmcrH4fhjFOoMIKEdF+x9EUhZ9CS/CIfVco7Y5wU=".to_string(), + }, + position: Position { + epoch: 746u64, + block: 237u64, + commitment: 0u64, + }, + }, + set_position: StoredPosition { + Position: Position { + epoch: 750u64, + block: 710u64, + commitment: 0u64, + }, + }, + set_forgotten: 3u64, + }; + + // Populate database with records (CRUD). + let tx_note: IdbTransaction = database_ref + .transaction_on_one_with_mode("SPENDABLE_NOTES", IdbTransactionMode::Readwrite) + .unwrap(); + let tx_tree_commitments: IdbTransaction = database_ref + .transaction_on_one_with_mode("TREE_COMMITMENTS", IdbTransactionMode::Readwrite) + .unwrap(); + let tx_tree_last_position: IdbTransaction = database_ref + .transaction_on_one_with_mode("TREE_LAST_POSITION", IdbTransactionMode::Readwrite) + .unwrap(); + let tx_tree_last_forgotten: IdbTransaction = database_ref + .transaction_on_one_with_mode("TREE_LAST_FORGOTTEN", IdbTransactionMode::Readwrite) + .unwrap(); + + let store_note: IdbObjectStore = tx_note.object_store("SPENDABLE_NOTES").unwrap(); + let store_tree_commitments: IdbObjectStore = tx_tree_commitments + .object_store("TREE_COMMITMENTS") + .unwrap(); + let store_tree_last_position: IdbObjectStore = tx_tree_last_position + .object_store("TREE_LAST_POSITION") + .unwrap(); + let store_tree_last_forgotten: IdbObjectStore = tx_tree_last_forgotten + .object_store("TREE_LAST_FORGOTTEN") + .unwrap(); + + let spendable_note_json = serde_wasm_bindgen::to_value(&spendable_note).unwrap(); + let tree_commitments_json = + serde_wasm_bindgen::to_value(&sctUpdates.store_commitments).unwrap(); + let tree_position_json_value = + serde_wasm_bindgen::to_value(&sctUpdates.set_position).unwrap(); + let tree_position_json_key = serde_wasm_bindgen::to_value(&"last_position").unwrap(); + let tree_last_forgotten_json_value = + serde_wasm_bindgen::to_value(&sctUpdates.set_forgotten).unwrap(); + let tree_last_forgotten_json_key: JsValue = + serde_wasm_bindgen::to_value(&"last_forgotten").unwrap(); + + store_note.put_val(&spendable_note_json).unwrap(); + store_tree_commitments + .put_val(&tree_commitments_json) + .unwrap(); + store_tree_last_position + .put_key_val(&tree_position_json_key, &tree_position_json_value) + .unwrap(); + store_tree_last_forgotten + .put_key_val( + &tree_last_forgotten_json_key, + &tree_last_forgotten_json_value, + ) + .unwrap(); + + // Set refund address. + #[derive(Clone, Debug, Serialize, Deserialize)] + struct RefundAddress { + inner: String, + } + let refund_address = RefundAddress { + inner: "ts1I61pd5+xWqlwcuPwsPOGbjevxAoQVymTXyHe60jLlY57WHcAuGsSwYuSxnOX+nTgEBm3MHn7mBlNTxqEkbnJwlNu6YUSDmA8D+aOqCT4=".to_string(), + }; + let refund_address_json: JsValue = serde_wasm_bindgen::to_value(&refund_address).unwrap(); + let source: JsValue = serde_wasm_bindgen::to_value(&None::).unwrap(); + + // -------------- 1. Query transaction plan performing a spend -------------- + + let transaction_plan: JsValue = wasm_planner + .plan(refund_address_json, source) + .await + .unwrap(); + + // -------------- 2. Generate authorization data from spend key and transaction plan -------------- + + let spend_key = + "penumbraspendkey1qul0huewkcmemljd5m3vz3awqt7442tjg2dudahvzu6eyj9qf0eszrnguh" + .to_string(); + + let authorization_data = authorize(&spend_key, transaction_plan.clone()).unwrap(); + + // -------------- 3. Generate witness -------------- + + // Retrieve SCT from storage. + let tx_last_position: IdbTransaction<'_> = database_ref + .transaction_on_one("TREE_LAST_POSITION") + .unwrap(); + let store_last_position = tx_last_position.object_store("TREE_LAST_POSITION").unwrap(); + let value_last_position: Option = store_last_position + .get_owned("last_position") + .unwrap() + .await + .unwrap(); + + let tx_last_forgotten = database_ref + .transaction_on_one("TREE_LAST_FORGOTTEN") + .unwrap(); + let store_last_forgotten = tx_last_forgotten + .object_store("TREE_LAST_FORGOTTEN") + .unwrap(); + let value_last_forgotten: Option = store_last_forgotten + .get_owned("last_forgotten") + .unwrap() + .await + .unwrap(); + + let tx_commitments = database_ref.transaction_on_one("TREE_COMMITMENTS").unwrap(); + let store_commitments = tx_commitments.object_store("TREE_COMMITMENTS").unwrap(); + let value_commitments = store_commitments + .get_owned("MY7PmcrH4fhjFOoMIKEdF+x9EUhZ9CS/CIfVco7Y5wU=") + .unwrap() + .await + .unwrap(); + + // Convert retrieved values to `JsValue`. + let last_position_json: StoredPosition = + serde_wasm_bindgen::from_value(value_last_position.unwrap()).unwrap(); + let last_forgotten_json: Forgotten = + serde_wasm_bindgen::from_value(value_last_forgotten.unwrap()).unwrap(); + let commitments_jsvalue: StoreCommitment = + serde_wasm_bindgen::from_value(JsValue::from(value_commitments.clone())).unwrap(); + + // Reconstruct SCT struct. + let mut vec_store_commitments: Vec = Vec::new(); + vec_store_commitments.push(commitments_jsvalue.clone()); + + let sct = StoredTree { + last_position: Some(last_position_json.clone()), + last_forgotten: Some(last_forgotten_json.clone()), + hashes: [].to_vec(), + commitments: vec_store_commitments, + }; + + // Convert SCT to `JsValue`. + let sct_json = serde_wasm_bindgen::to_value(&sct).unwrap(); + + // Generate witness data from SCT and specific transaction plan. + let witness_data: Result = witness(transaction_plan.clone(), sct_json); + + // Viewing key to reveal asset balances and transactions. + let full_viewing_key = "penumbrafullviewingkey1mnm04x7yx5tyznswlp0sxs8nsxtgxr9p98dp0msuek8fzxuknuzawjpct8zdevcvm3tsph0wvsuw33x2q42e7sf29q904hwerma8xzgrxsgq2"; + + // Serialize transaction plan into `TransactionPlan`. + let transaction_plan_serialized: tp = + serde_wasm_bindgen::from_value(transaction_plan.clone()).unwrap(); + let transaction_plan_conv: TransactionPlan = + transaction_plan_serialized.try_into().unwrap(); + + // -------------- 4. Build the (1) Serial Transaction and (2) Parallel Transaction -------------- + + let mut actions: Vec = Vec::new(); + + for i in transaction_plan_conv.actions.clone() { + if let ActionPlan::Spend(ref _spend_plan) = i { + let action_deserialize = serde_wasm_bindgen::to_value(&i).unwrap(); + let action = wasm_planner + .build_action( + transaction_plan.clone(), + action_deserialize, + full_viewing_key, + witness_data.as_ref().unwrap().clone(), + ) + .unwrap(); + let action_serialize: Action = + serde_wasm_bindgen::from_value(action.clone()).unwrap(); + actions.push(action_serialize); + } + if let ActionPlan::Output(ref _output_plan) = i { + let action_deserialize = serde_wasm_bindgen::to_value(&i).unwrap(); + let action = wasm_planner + .build_action( + transaction_plan.clone(), + action_deserialize, + full_viewing_key, + witness_data.as_ref().unwrap().clone(), + ) + .unwrap(); + let action_serialize: Action = + serde_wasm_bindgen::from_value(action.clone()).unwrap(); + actions.push(action_serialize); + } + } + + // Deserialize actions. + let action_deserialized = serde_wasm_bindgen::to_value(&actions).unwrap(); + + // Execute parallel spend transaction and generate proof. + let parallel_transaction = build_parallel( + action_deserialized, + transaction_plan.clone(), + witness_data.as_ref().unwrap().clone(), + authorization_data.clone(), + ) + .unwrap(); + console_log!("Parallel transaction is: {:?}", parallel_transaction); + + let serial_transaction = build( + full_viewing_key, + transaction_plan.clone(), + witness_data.as_ref().unwrap().clone(), + authorization_data.clone(), + ) + .unwrap(); + console_log!("Serial transaction is: {:?}", serial_transaction); + } +}