From 7f67617fd824394112d92c12c1f2dc42b8af342d Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Fri, 29 Mar 2024 00:01:15 -0700 Subject: [PATCH 01/14] Implement a newtype for Precision --- Cargo.lock | 1 + "\\" | 641 ++++++++++++++++++ crates/core/app/Cargo.toml | 3 +- .../app/src/action_handler/transaction.rs | 2 +- .../action_handler/transaction/stateful.rs | 10 +- ..._can_define_and_delegate_to_a_validator.rs | 7 +- .../app_can_deposit_into_community_pool.rs | 2 +- .../app_can_disable_community_pool_spends.rs | 6 +- .../app_can_propose_community_pool_spends.rs | 6 +- .../app_can_spend_notes_and_detect_outputs.rs | 3 +- .../app_can_undelegate_from_a_validator.rs | 7 +- ...ator_definitions_with_invalid_auth_sigs.rs | 2 +- ..._uptime_for_validators_only_once_active.rs | 7 +- .../core/component/shielded-pool/src/fmd.rs | 11 +- crates/core/transaction/src/action_list.rs | 2 +- crates/core/transaction/src/plan.rs | 9 +- crates/core/transaction/src/plan/clue.rs | 14 +- crates/crypto/decaf377-fmd/benches/fmd.rs | 13 +- crates/crypto/decaf377-fmd/src/clue.rs | 26 +- crates/crypto/decaf377-fmd/src/clue_key.rs | 17 +- crates/crypto/decaf377-fmd/src/detection.rs | 9 +- crates/crypto/decaf377-fmd/src/error.rs | 2 +- crates/crypto/decaf377-fmd/src/lib.rs | 6 +- crates/crypto/decaf377-fmd/src/precision.rs | 79 +++ crates/crypto/decaf377-fmd/tests/fmd.rs | 17 +- crates/proto/src/protobuf.rs | 10 +- 26 files changed, 826 insertions(+), 86 deletions(-) create mode 100644 "\\" create mode 100644 crates/crypto/decaf377-fmd/src/precision.rs diff --git a/Cargo.lock b/Cargo.lock index 8afc6c9e5a..0b0a41fb3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4675,6 +4675,7 @@ dependencies = [ "cnidarium", "cnidarium-component", "decaf377 0.5.0", + "decaf377-fmd", "decaf377-rdsa", "ed25519-consensus", "futures", diff --git "a/\\" "b/\\" new file mode 100644 index 0000000000..1bbf40a1b3 --- /dev/null +++ "b/\\" @@ -0,0 +1,641 @@ +use std::{ + collections::BTreeMap, + fmt::{self, Debug, Formatter}, + mem, +}; + +use anyhow::{Context, Result}; +use penumbra_sct::epoch::Epoch; +use rand::{CryptoRng, RngCore}; +use rand_core::OsRng; +use tracing::instrument; + +use crate::{SpendableNoteRecord, ViewClient}; +use anyhow::anyhow; +use penumbra_asset::{asset, Value}; +use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan; +use penumbra_auction::auction::dutch::DutchAuctionDescription; +use penumbra_auction::auction::{ + dutch::actions::{ActionDutchAuctionEnd, ActionDutchAuctionSchedule}, + AuctionId, +}; +use penumbra_community_pool::CommunityPoolDeposit; +use penumbra_dex::{ + lp::action::{PositionClose, PositionOpen}, + lp::plan::PositionWithdrawPlan, + lp::position::{self, Position}, + lp::Reserves, + swap::SwapPlaintext, + swap::SwapPlan, + swap_claim::SwapClaimPlan, + TradingPair, +}; +use penumbra_fee::{Fee, FeeTier, GasPrices}; +use penumbra_governance::{ + proposal_state, DelegatorVotePlan, Proposal, ProposalDepositClaim, ProposalSubmit, + ProposalWithdraw, ValidatorVote, Vote, +}; +use penumbra_ibc::IbcRelay; +use penumbra_keys::{keys::AddressIndex, Address}; +use penumbra_num::Amount; +use penumbra_proto::view::v1::{NotesForVotingRequest, NotesRequest}; +use penumbra_shielded_pool::{Ics20Withdrawal, Note, OutputPlan, SpendPlan}; +use penumbra_stake::{rate::RateData, validator, IdentityKey, UndelegateClaimPlan}; +use penumbra_tct as tct; +use penumbra_transaction::{ + memo::MemoPlaintext, + plan::{ActionPlan, MemoPlan, TransactionPlan}, + ActionList, TransactionParameters, +}; + +/// A planner for a [`TransactionPlan`] that can fill in the required spends and change outputs upon +/// finalization to make a transaction balance. +pub struct Planner { + rng: R, + action_list: ActionList, + /// The fee tier to apply to this transaction. + fee_tier: FeeTier, + /// The set of prices used for gas estimation. + gas_prices: GasPrices, + /// The transaction parameters to use for the transaction. + transaction_parameters: TransactionParameters, + /// A user-specified change address, if any. + change_address: Option
, + /// A user-specified memo text, if any. + memo_text: Option, + /// A user-specified memo return address, if any. + memo_return_address: Option
, +} + +impl Debug for Planner { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Planner") + .field("action_list", &self.action_list) + .field("fee_tier", &self.fee_tier) + .field("gas_prices", &self.gas_prices) + .field("transaction_parameters", &self.transaction_parameters) + .field("change_address", &self.change_address) + .field("memo_text", &self.memo_text) + .field("memo_return_address", &self.memo_return_address) + .finish() + } +} + +impl Planner { + /// Create a new planner. + pub fn new(rng: R) -> Self { + Self { + rng, + action_list: Default::default(), + gas_prices: Default::default(), + fee_tier: Default::default(), + transaction_parameters: Default::default(), + change_address: None, + memo_text: None, + memo_return_address: None, + } + } + + /// Add an arbitrary action to the planner. + pub fn action>(&mut self, action: A) -> &mut Self { + self.action_list.push(action); + self + } + + /// Set the current gas prices for fee prediction. + #[instrument(skip(self))] + pub fn set_gas_prices(&mut self, gas_prices: GasPrices) -> &mut Self { + self.gas_prices = gas_prices; + self + } + + /// Set the fee tier. + #[instrument(skip(self))] + pub fn set_fee_tier(&mut self, fee_tier: FeeTier) -> &mut Self { + self.fee_tier = fee_tier; + self + } + + /// Set the expiry height for the transaction. + #[instrument(skip(self))] + pub fn expiry_height(&mut self, expiry_height: u64) -> &mut Self { + self.transaction_parameters.expiry_height = expiry_height; + self + } + + /// Set a human-readable memo text for the transaction. + /// + /// Errors if the memo is too long. + #[instrument(skip(self))] + pub fn memo(&mut self, text: String) -> &mut Self { + self.memo_text = Some(text); + self + } + + /// Customize the return address for the memo. + /// + /// If unset, this will default to the address for the source account. This + /// must be an address controlled by the user, as the expectation is that + /// the recipient can use the address to transact with the sender. + #[instrument(skip(self))] + pub fn memo_return_address(&mut self, address: Address) -> &mut Self { + self.memo_return_address = Some(address); + self + } + + /// Set the change address for the transaction. + /// + /// If unset, this will default to the address for the source account. + /// + /// This can be a foreign address, allowing "send max" functionality. + #[instrument(skip(self))] + pub fn change_address(&mut self, address: Address) -> &mut Self { + self.change_address = Some(address); + self + } + + /// Spend a specific positioned note in the transaction. + #[instrument(skip(self))] + pub fn spend(&mut self, note: Note, position: tct::Position) -> &mut Self { + self.action_list + .push(SpendPlan::new(&mut self.rng, note, position)); + self + } + + /// Add an output note from this transaction. + /// + /// Any unused output value will be redirected back to the originating address as change notes. + #[instrument(skip(self))] + pub fn output(&mut self, value: Value, address: Address) -> &mut Self { + self.action_list + .push(OutputPlan::new(&mut self.rng, value, address)); + self + } + + /// Open a liquidity position in the order book. + #[instrument(skip(self))] + pub fn position_open(&mut self, position: Position) -> &mut Self { + self.action_list.push(PositionOpen { position }); + self + } + + /// Close a liquidity position in the order book. + #[instrument(skip(self))] + pub fn position_close(&mut self, position_id: position::Id) -> &mut Self { + self.action_list.push(PositionClose { position_id }); + self + } + + /// Withdraw a liquidity position in the order book. + /// + /// Note: Currently this only supports an initial withdrawal from Closed, with no rewards. + #[instrument(skip(self))] + pub fn position_withdraw( + &mut self, + position_id: position::Id, + reserves: Reserves, + pair: TradingPair, + ) -> &mut Self { + self.action_list.push(PositionWithdrawPlan { + reserves, + position_id, + pair, + sequence: 0, + rewards: Vec::new(), + }); + self + } + + /// Schedule a Dutch auction. + #[instrument(skip(self))] + pub fn dutch_auction_schedule(&mut self, description: DutchAuctionDescription) -> &mut Self { + self.action_list + .push(ActionDutchAuctionSchedule { description }); + self + } + + /// Ends a Dutch auction. + #[instrument(skip(self))] + pub fn dutch_auction_end(&mut self, auction_id: AuctionId) -> &mut Self { + self.action_list.push(ActionDutchAuctionEnd { auction_id }); + self + } + + /// Withdraws the reserves of the Dutch auction. + // TODO: nicer api? what do we get by passing fields individually rather than the plan? + #[instrument(skip(self))] + pub fn dutch_auction_withdraw(&mut self, plan: ActionDutchAuctionWithdrawPlan) -> &mut Self { + self.action_list.push(plan); + self + } + + /// Perform a swap based on input notes in the transaction. + #[instrument(skip(self))] + pub fn swap( + &mut self, + input_value: Value, + into_asset: asset::Id, + swap_claim_fee: Fee, + claim_address: Address, + ) -> Result<&mut Self> { + // Determine the canonical order for the assets being swapped. + // This will determine whether the input amount is assigned to delta_1 or delta_2. + let trading_pair = TradingPair::new(input_value.asset_id, into_asset); + + // If `trading_pair.asset_1` is the input asset, then `delta_1` is the input amount, + // and `delta_2` is 0. + // + // Otherwise, `delta_1` is 0, and `delta_2` is the input amount. + let (delta_1, delta_2) = if trading_pair.asset_1() == input_value.asset_id { + (input_value.amount, 0u64.into()) + } else { + (0u64.into(), input_value.amount) + }; + + // If there is no input, then there is no swap. + if delta_1 == Amount::zero() && delta_2 == Amount::zero() { + anyhow::bail!("No input value for swap"); + } + + // Create the `SwapPlaintext` representing the swap to be performed: + let swap_plaintext = SwapPlaintext::new( + &mut self.rng, + trading_pair, + delta_1, + delta_2, + swap_claim_fee, + claim_address, + ); + + let swap = SwapPlan::new(&mut self.rng, swap_plaintext); + self.action_list.push(swap); + + Ok(self) + } + + /// Perform a swap claim based on an input swap with a pre-paid fee. + #[instrument(skip(self))] + pub fn swap_claim(&mut self, plan: SwapClaimPlan) -> &mut Self { + self.action_list.push(plan); + self + } + + /// Add a delegation to this transaction. + #[instrument(skip(self))] + pub fn delegate( + &mut self, + epoch: Epoch, + unbonded_amount: Amount, + rate_data: RateData, + ) -> &mut Self { + let delegation = rate_data.build_delegate(epoch, unbonded_amount); + self.action_list.push(delegation); + self + } + + /// Add an undelegation to this transaction. + #[instrument(skip(self))] + pub fn undelegate( + &mut self, + epoch: Epoch, + delegation_amount: Amount, + rate_data: RateData, + ) -> &mut Self { + let undelegation = rate_data.build_undelegate(epoch, delegation_amount); + self.action_list.push(undelegation); + self + } + + /// Add an undelegate claim to this transaction. + #[instrument(skip(self))] + pub fn undelegate_claim(&mut self, claim_plan: UndelegateClaimPlan) -> &mut Self { + self.action_list.push(claim_plan); + self + } + + /// Upload a validator definition in this transaction. + #[instrument(skip(self))] + pub fn validator_definition(&mut self, new_validator: validator::Definition) -> &mut Self { + self.action_list.push(new_validator); + self + } + + /// Submit a new governance proposal in this transaction. + #[instrument(skip(self))] + pub fn proposal_submit(&mut self, proposal: Proposal, deposit_amount: Amount) -> &mut Self { + self.action_list.push(ProposalSubmit { + proposal, + deposit_amount, + }); + self + } + + /// Withdraw a governance proposal in this transaction. + #[instrument(skip(self))] + pub fn proposal_withdraw(&mut self, proposal: u64, reason: String) -> &mut Self { + self.action_list.push(ProposalWithdraw { proposal, reason }); + self + } + + /// Claim a governance proposal deposit in this transaction. + #[instrument(skip(self))] + pub fn proposal_deposit_claim( + &mut self, + proposal: u64, + deposit_amount: Amount, + outcome: proposal_state::Outcome<()>, + ) -> &mut Self { + self.action_list.push(ProposalDepositClaim { + proposal, + deposit_amount, + outcome, + }); + self + } + + /// Deposit a value into the Community Pool. + #[instrument(skip(self))] + pub fn community_pool_deposit(&mut self, value: Value) -> &mut Self { + self.action_list.push(CommunityPoolDeposit { value }); + self + } + + /// Cast a validator vote in this transaction. + #[instrument(skip(self))] + pub fn validator_vote(&mut self, vote: ValidatorVote) -> &mut Self { + self.action_list.push(vote); + self + } + + /// Perform an ICS-20 withdrawal + #[instrument(skip(self))] + pub fn ics20_withdrawal(&mut self, withdrawal: Ics20Withdrawal) -> &mut Self { + self.action_list.push(withdrawal); + self + } + + /// Perform an IBC action + #[instrument(skip(self))] + pub fn ibc_action(&mut self, ibc_action: IbcRelay) -> &mut Self { + self.action_list.push(ibc_action); + self + } + + /// Vote with all possible vote weight on a given proposal. + #[instrument(skip_all)] + pub async fn delegator_vote( + // TODO this sucks, why isn't there a bundle of proposal data to use for voting + // how is that not the thing returned by the rpc? why do we have to query a bunch of shit + // independently and stitch it together? + &mut self, + view: &mut V, + source: AddressIndex, + proposal: u64, + vote: Vote, + start_block_height: u64, + start_position: tct::Position, + start_rate_data: BTreeMap, + ) -> Result<&mut Self, anyhow::Error> { + let voting_notes = view + .notes_for_voting(NotesForVotingRequest { + votable_at_height: start_block_height, + address_index: Some(source.into()), + }) + .await?; + + anyhow::ensure!( + !voting_notes.is_empty(), + "no notes were found for voting on proposal {}", + proposal + ); + + // 1. Create a DelegatorVotePlan for each votable note. + for (record, ik) in &voting_notes { + let validator_start_rate_data = start_rate_data + .get(&ik) + .ok_or_else(|| anyhow!("missing rate data for votable note delegated to {}", ik))?; + + let voting_power_at_vote_start = + validator_start_rate_data.unbonded_amount(record.note.amount()); + + // 1. Create a DelegatorVotePlan that votes with this note on the proposal. + let plan = DelegatorVotePlan::new( + &mut self.rng, + proposal, + start_position, + vote, + record.note.clone(), + record.position, + voting_power_at_vote_start, + ); + self.delegator_vote_precise(plan); + } + + // 2. Here, we could sweep any spendable notes with delegation tokens to + // a new output to try to unlink them from a future vote. In practice + // this is meaningless because we don't have flow encryption, so + // delegator votes reveal the precise amount, and this amount will + // likely be unique to the delegator and enough to link their votes. + // Also, because we're in a single transaction, the pattern of + // delegations will also be revealed (vs creating distinct transactions + // for each validator). + // + // So instead, we do nothing. + + Ok(self) + } + + /// Vote with a specific positioned note in the transaction, rather than automatically. + #[instrument(skip(self, plan))] + pub fn delegator_vote_precise(&mut self, plan: DelegatorVotePlan) -> &mut Self { + self.action_list.push(plan); + self + } + + /// Prioritize notes to spend to release value of a specific transaction. + /// + /// Various logic is possible for note selection. Currently, this method + /// prioritizes notes sent to a one-time address, then notes with the largest + /// value: + /// + /// - Prioritizing notes sent to one-time addresses optimizes for a future in + /// which we implement DAGSync keyed by fuzzy message detection (which will not + /// be able to detect notes sent to one-time addresses). Spending these notes + /// immediately converts them into change notes, sent to the default address for + /// the users' account, which are detectable. + /// + /// - Prioritizing notes with the largest value optimizes for gas used by the + /// transaction. + /// + /// We may want to make note prioritization configurable in the future. For + /// instance, a user might prefer a note prioritization strategy that harvested + /// capital losses when possible, using cost basis information retained by the + /// view server. + pub fn prioritize_and_filter_spendable_notes( + &mut self, + records: Vec, + ) -> Vec { + let mut filtered = records + .into_iter() + .filter(|record| record.note.amount() > Amount::zero()) + .collect::>(); + filtered.sort_by(|a, b| { + // Sort by whether the note was sent to an ephemeral address... + match ( + a.address_index.is_ephemeral(), + b.address_index.is_ephemeral(), + ) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + // ... then by largest amount. + _ => b.note.amount().cmp(&a.note.amount()), + } + }); + filtered + } + + /// Add spends and change outputs as required to balance the transaction, using the view service + /// provided to supply the notes and other information. + pub async fn plan( + &mut self, + view: &mut V, + mut source: AddressIndex, + ) -> anyhow::Result { + // Wipe out the randomizer for the provided source, since + // 1. All randomizers correspond to the same account + // 2. Using one-time addresses for change addresses is undesirable. + source.randomizer = [0u8; 12]; + + // Compute the change address for this transaction. + let change_address = if let Some(ref address) = self.change_address { + address.clone() + } else { + view.address_by_index(source).await?.clone() + }; + + // Phase 1, "process all of the user-supplied intents into complete + // action plans", has already happened using the builder API. + // + // Compute an initial fee estimate based on the actions we have so far. + self.action_list.refresh_fee_and_change( + &mut self.rng, + &self.gas_prices, + &self.fee_tier, + &change_address, + ); + + // Phase 2: balance the transaction with information from the view service. + // + // It's possible that adding spends could increase the gas, increasing + // the fee amount, and so on, so we add spends iteratively. However, we + // need to query all the notes we'll use for planning upfront, so we + // don't accidentally try to use the same one twice. + + let mut notes_by_asset_id = BTreeMap::new(); + for required in self.action_list.balance_with_fee().required() { + // Find all the notes of this asset in the source account. + let records: Vec = view + .notes(NotesRequest { + include_spent: false, + asset_id: Some(required.asset_id.into()), + address_index: Some(source.into()), + amount_to_spend: None, + }) + .await?; + notes_by_asset_id.insert( + required.asset_id, + self.prioritize_and_filter_spendable_notes(records), + ); + } + + let mut iterations = 0usize; + + // Now iterate over the action list's imbalances to balance the transaction. + while let Some(required) = self.action_list.balance_with_fee().required().next() { + // Find a single note to spend towards the required balance. + let note = notes_by_asset_id + .get_mut(&required.asset_id) + .expect("we already made a notesrequest for each required asset") + .pop() + .ok_or_else(|| { + anyhow!( + "ran out of notes to spend while planning transaction, need {} of asset {}", + required.amount, + required.asset_id, + ) + })?; + + // Add a spend for that note to the action list. + self.action_list + .push(SpendPlan::new(&mut OsRng, note.note, note.position)); + + // Refresh the fee estimate and change outputs. + self.action_list.refresh_fee_and_change( + &mut self.rng, + &self.gas_prices, + &self.fee_tier, + &change_address, + ); + + iterations = iterations + 1; + if iterations > 100 { + return Err(anyhow!("failed to plan transaction after 100 iterations")); + } + } + + // Construct the memo plan for the transaction, using user-specified data if it + // was provided. + let memo_plan = if self.action_list.requires_memo() { + let return_address = if let Some(ref address) = self.memo_return_address { + // Check that this address is actually controlled by the user. + // We don't have an FVK, so we have to ask the view service. + anyhow::ensure!( + view.index_by_address(address.clone()).await?.is_some(), + "return address for memo is not controlled by the user", + ); + address.clone() + } else { + view.address_by_index(source).await?.clone() + }; + + Some(MemoPlan::new( + &mut self.rng, + MemoPlaintext::new(return_address, self.memo_text.take().unwrap_or_default()) + .context("could not create memo plaintext")?, + )) + } else { + None + }; + + // Configure the transaction parameters with the chain ID. + let app_params = view.app_params().await?; + let chain_id = app_params.chain_id.clone(); + self.transaction_parameters.chain_id = chain_id.clone(); + + // Fetch the FMD parameters that will be used to plan the transaction. + // (This really should have been considered witness data. Oh well.) + let fmd_params = view.fmd_parameters().await?; + + let plan = mem::take(&mut self.action_list).into_plan( + &mut self.rng, + &fmd_params, + self.transaction_parameters.clone(), + memo_plan, + )?; + + // Reset the planner in case it were reused. We don't want people to do that + // but otherwise we can't do builder method chaining with &mut self, and forcing + // the builder to move between calls is annoying for callers who are building up + // actions programmatically. Except we can't do a normal std::mem::replace here because + // the generic RNG mucks everything up. So it's just awful. + self.action_list = Default::default(); + self.gas_prices = Default::default(); + self.fee_tier = Default::default(); + self.transaction_parameters = Default::default(); + self.change_address = None; + self.memo_text = None; + self.memo_return_address = None; + + Ok(plan) + } +} diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index f8b230f2a9..3fd185ca95 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -54,6 +54,7 @@ penumbra-shielded-pool = { workspace = true, features = ["component"], penumbra-stake = { workspace = true, default-features = true } penumbra-tct = { workspace = true, default-features = true } penumbra-tendermint-proxy = { path = "../../util/tendermint-proxy" } +penumbra-test-subscriber = { workspace = true } penumbra-tower-trace = { path = "../../util/tower-trace" } penumbra-transaction = { workspace = true, features = ["parallel"], default-features = true } penumbra-txhash = { workspace = true, default-features = true } @@ -82,10 +83,10 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] +decaf377-fmd = { workspace = true, default-features = true } ed25519-consensus = { workspace = true } penumbra-mock-consensus = { workspace = true } penumbra-mock-client = { workspace = true } -penumbra-test-subscriber = { workspace = true } rand = { workspace = true } rand_core = { workspace = true } rand_chacha = { workspace = true } diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index b289c0938a..e454895f40 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -172,7 +172,7 @@ mod tests { clue_plans: vec![CluePlan::new( &mut OsRng, test_keys::ADDRESS_1.deref().clone(), - 1, + 1.try_into().unwrap(), )], }), memo: None, diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index df65858396..2a7c6f8285 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -93,8 +93,8 @@ pub async fn fmd_parameters_valid(state: S, transaction: &Transact #[tracing::instrument( skip_all, fields( - current_fmd.precision_bits = current_fmd_parameters.precision_bits, - previous_fmd.precision_bits = previous_fmd_parameters.precision_bits, + current_fmd.precision_bits = current_fmd_parameters.precision.bits(), + previous_fmd.precision_bits = previous_fmd_parameters.precision.bits(), previous_fmd.as_of_block_height = previous_fmd_parameters.as_of_block_height, block_height, ) @@ -113,9 +113,9 @@ pub fn fmd_precision_within_grace_period( { // Clue must be using the current `fmd::Parameters`, or be within // `FMD_GRACE_PERIOD_BLOCKS` of the previous `fmd::Parameters`. - let clue_precision = clue.precision_bits(); - let using_current_precision = clue_precision == current_fmd_parameters.precision_bits; - let using_previous_precision = clue_precision == previous_fmd_parameters.precision_bits; + let clue_precision = clue.precision()?; + let using_current_precision = clue_precision == current_fmd_parameters.precision; + let using_previous_precision = clue_precision == previous_fmd_parameters.precision; let within_grace_period = block_height < previous_fmd_parameters.as_of_block_height + FMD_GRACE_PERIOD_BLOCKS; if using_current_precision || (using_previous_precision && within_grace_period) { diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index f3ccc4d4a7..f01b769295 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, TestNodeExt, ValidatorDataReadExt}, anyhow::anyhow, cnidarium::TempStorage, + decaf377_fmd::Precision, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ genesis::{self, AppState}, @@ -157,7 +158,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; @@ -270,7 +271,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; @@ -432,7 +433,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; diff --git a/crates/core/app/tests/app_can_deposit_into_community_pool.rs b/crates/core/app/tests/app_can_deposit_into_community_pool.rs index 3c927dca19..c2b69a7d04 100644 --- a/crates/core/app/tests/app_can_deposit_into_community_pool.rs +++ b/crates/core/app/tests/app_can_deposit_into_community_pool.rs @@ -81,7 +81,7 @@ async fn app_can_deposit_into_community_pool() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. diff --git a/crates/core/app/tests/app_can_disable_community_pool_spends.rs b/crates/core/app/tests/app_can_disable_community_pool_spends.rs index 7a9b8e9577..e8e4e1ab97 100644 --- a/crates/core/app/tests/app_can_disable_community_pool_spends.rs +++ b/crates/core/app/tests/app_can_disable_community_pool_spends.rs @@ -184,7 +184,7 @@ async fn app_can_disable_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. @@ -253,7 +253,7 @@ async fn app_can_disable_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. @@ -288,7 +288,7 @@ async fn app_can_disable_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. diff --git a/crates/core/app/tests/app_can_propose_community_pool_spends.rs b/crates/core/app/tests/app_can_propose_community_pool_spends.rs index a7fbd25f69..0f77a8a757 100644 --- a/crates/core/app/tests/app_can_propose_community_pool_spends.rs +++ b/crates/core/app/tests/app_can_propose_community_pool_spends.rs @@ -178,7 +178,7 @@ async fn app_can_propose_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. @@ -247,7 +247,7 @@ async fn app_can_propose_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. @@ -282,7 +282,7 @@ async fn app_can_propose_community_pool_spends() -> anyhow::Result<()> { }, } }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Default::default()); let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. diff --git a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index 517a9d95c8..bdf9bac193 100644 --- a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -6,6 +6,7 @@ use { genesis::{self, AppState}, server::consensus::Consensus, }, + decaf377_fmd::Precision, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -87,7 +88,7 @@ async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(OsRng, 0); + plan.populate_detection_data(OsRng, Precision::default()); let tx = client.witness_auth_build(&plan).await?; diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index fba7559b3f..e381abf835 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -3,6 +3,7 @@ use { anyhow::anyhow, ark_ff::UniformRand, cnidarium::TempStorage, + decaf377_fmd::Precision, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -149,7 +150,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); (plan, note, staking_note_nullifier) }; let tx = client.witness_auth_build(&plan).await?; @@ -249,7 +250,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); (plan, undelegate_token_id) }; let tx = client.witness_auth_build(&plan).await?; @@ -333,7 +334,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; diff --git a/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs index 4013a20524..8242d817fa 100644 --- a/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs +++ b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs @@ -113,7 +113,7 @@ async fn app_rejects_validator_definitions_with_invalid_auth_sigs() -> anyhow::R ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Default::default()); plan }; let tx = client.witness_auth_build(&plan).await?; diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 1a7f66b18b..51c852dced 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -1,6 +1,7 @@ use { self::common::{BuilderExt, TestNodeExt, ValidatorDataReadExt}, cnidarium::TempStorage, + decaf377_fmd::Precision, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ genesis::{self, AppState}, @@ -133,7 +134,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; @@ -210,7 +211,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; @@ -334,7 +335,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ..Default::default() }, }; - plan.populate_detection_data(rand_core::OsRng, 0); + plan.populate_detection_data(rand_core::OsRng, Precision::default()); plan }; let tx = client.witness_auth_build(&plan).await?; diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index 58ae8bce2f..c363520ae4 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,3 +1,4 @@ +use decaf377_fmd::Precision; use penumbra_proto::{core::component::shielded_pool::v1 as pb, DomainType}; use serde::{Deserialize, Serialize}; @@ -6,8 +7,8 @@ pub mod state_key; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "pb::FmdParameters", into = "pb::FmdParameters")] pub struct Parameters { - /// Bits of precision. - pub precision_bits: u8, + /// FMD Precision. + pub precision: Precision, /// The block height at which these parameters became effective. pub as_of_block_height: u64, } @@ -21,7 +22,7 @@ impl TryFrom for Parameters { fn try_from(msg: pb::FmdParameters) -> Result { Ok(Parameters { - precision_bits: msg.precision_bits.try_into()?, + precision: msg.precision_bits.try_into()?, as_of_block_height: msg.as_of_block_height, }) } @@ -30,7 +31,7 @@ impl TryFrom for Parameters { impl From for pb::FmdParameters { fn from(params: Parameters) -> Self { pb::FmdParameters { - precision_bits: u32::from(params.precision_bits), + precision_bits: params.precision.bits() as u32, as_of_block_height: params.as_of_block_height, } } @@ -39,7 +40,7 @@ impl From for pb::FmdParameters { impl Default for Parameters { fn default() -> Self { Self { - precision_bits: 0, + precision: Precision::default(), as_of_block_height: 1, } } diff --git a/crates/core/transaction/src/action_list.rs b/crates/core/transaction/src/action_list.rs index f61f25731c..d1d0b1c2fc 100644 --- a/crates/core/transaction/src/action_list.rs +++ b/crates/core/transaction/src/action_list.rs @@ -60,7 +60,7 @@ impl ActionList { memo: memo_plan, detection_data: None, }; - plan.populate_detection_data(rng, fmd_params.precision_bits.into()); + plan.populate_detection_data(rng, fmd_params.precision); // Implement a canonical ordering to the actions within the transaction // plan to reduce client distinguishability. diff --git a/crates/core/transaction/src/plan.rs b/crates/core/transaction/src/plan.rs index 5935c292c0..8e08782396 100644 --- a/crates/core/transaction/src/plan.rs +++ b/crates/core/transaction/src/plan.rs @@ -2,6 +2,7 @@ //! creation. use anyhow::Result; +use decaf377_fmd::Precision; use penumbra_community_pool::{CommunityPoolDeposit, CommunityPoolOutput, CommunityPoolSpend}; use penumbra_dex::{ lp::action::{PositionClose, PositionOpen}, @@ -359,19 +360,19 @@ impl TransactionPlan { pub fn populate_detection_data( &mut self, mut rng: R, - precision_bits: usize, + precision: Precision, ) { // Add one clue per recipient. let mut clue_plans = vec![]; for dest_address in self.dest_addresses() { - clue_plans.push(CluePlan::new(&mut rng, dest_address, precision_bits)); + clue_plans.push(CluePlan::new(&mut rng, dest_address, precision)); } // Now add dummy clues until we have one clue per output. let num_dummy_clues = self.num_outputs() - clue_plans.len(); for _ in 0..num_dummy_clues { let dummy_address = Address::dummy(&mut rng); - clue_plans.push(CluePlan::new(&mut rng, dummy_address, precision_bits)); + clue_plans.push(CluePlan::new(&mut rng, dummy_address, precision)); } if !clue_plans.is_empty() { @@ -529,7 +530,7 @@ mod tests { chain_id: "penumbra-test".to_string(), }, detection_data: Some(DetectionDataPlan { - clue_plans: vec![CluePlan::new(&mut OsRng, addr, 1)], + clue_plans: vec![CluePlan::new(&mut OsRng, addr, 1.try_into().unwrap())], }), memo: Some(MemoPlan::new(&mut OsRng, memo_plaintext.clone())), }; diff --git a/crates/core/transaction/src/plan/clue.rs b/crates/core/transaction/src/plan/clue.rs index a9883cdfc1..66e648da95 100644 --- a/crates/core/transaction/src/plan/clue.rs +++ b/crates/core/transaction/src/plan/clue.rs @@ -1,4 +1,4 @@ -use decaf377_fmd::Clue; +use decaf377_fmd::{Clue, Precision}; use penumbra_keys::Address; use penumbra_proto::{core::transaction::v1 as pb, DomainType}; @@ -7,7 +7,7 @@ use rand::{CryptoRng, RngCore}; #[derive(Clone, Debug)] pub struct CluePlan { pub address: Address, - pub precision_bits: usize, + pub precision: Precision, pub rseed: [u8; 32], } @@ -16,14 +16,14 @@ impl CluePlan { pub fn new( rng: &mut R, address: Address, - precision_bits: usize, + precision: Precision, ) -> CluePlan { let mut rseed = [0u8; 32]; rng.fill_bytes(&mut rseed); CluePlan { address, rseed, - precision_bits, + precision, } } @@ -32,7 +32,7 @@ impl CluePlan { let clue_key = self.address.clue_key(); let expanded_clue_key = clue_key.expand_infallible(); expanded_clue_key - .create_clue_deterministic(self.precision_bits, self.rseed) + .create_clue_deterministic(self.precision, self.rseed) .expect("can construct clue key") } } @@ -46,7 +46,7 @@ impl From for pb::CluePlan { Self { address: Some(msg.address.into()), rseed: msg.rseed.to_vec(), - precision_bits: msg.precision_bits as u64, + precision_bits: msg.precision.bits() as u64, } } } @@ -60,7 +60,7 @@ impl TryFrom for CluePlan { .ok_or_else(|| anyhow::anyhow!("missing address"))? .try_into()?, rseed: msg.rseed.as_slice().try_into()?, - precision_bits: msg.precision_bits.try_into()?, + precision: msg.precision_bits.try_into()?, }) } } diff --git a/crates/crypto/decaf377-fmd/benches/fmd.rs b/crates/crypto/decaf377-fmd/benches/fmd.rs index 3ae7ef60d0..7527526c28 100644 --- a/crates/crypto/decaf377-fmd/benches/fmd.rs +++ b/crates/crypto/decaf377-fmd/benches/fmd.rs @@ -1,4 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use fmd::Precision; use rand_core::OsRng; use decaf377_fmd as fmd; @@ -7,7 +8,7 @@ fn detect_clues(dk: &fmd::DetectionKey, clues: &[fmd::Clue]) -> usize { clues.iter().filter(|clue| dk.examine(clue)).count() } -fn create_clues(ck: &fmd::ExpandedClueKey, precision: usize) -> Vec { +fn create_clues(ck: &fmd::ExpandedClueKey, precision: Precision) -> Vec { (0..1024) .map(|_| { ck.create_clue(precision, OsRng) @@ -24,11 +25,11 @@ fn bench(c: &mut Criterion) { .expect("clue key bytes must be valid"); let clues = vec![ - (4, create_clues(&ck, 4)), - (5, create_clues(&ck, 5)), - (6, create_clues(&ck, 6)), - (7, create_clues(&ck, 7)), - (8, create_clues(&ck, 8)), + (4, create_clues(&ck, 4.try_into().unwrap())), + (5, create_clues(&ck, 5.try_into().unwrap())), + (6, create_clues(&ck, 6.try_into().unwrap())), + (7, create_clues(&ck, 7.try_into().unwrap())), + (8, create_clues(&ck, 8.try_into().unwrap())), ]; let mut group = c.benchmark_group("fmd-detection"); diff --git a/crates/crypto/decaf377-fmd/src/clue.rs b/crates/crypto/decaf377-fmd/src/clue.rs index 4734978550..1b965396e5 100644 --- a/crates/crypto/decaf377-fmd/src/clue.rs +++ b/crates/crypto/decaf377-fmd/src/clue.rs @@ -1,10 +1,28 @@ +use std::array::TryFromSliceError; + +use crate::{error::Error, Precision}; + /// A clue that allows probabilistic message detection. #[derive(Debug, Clone)] -pub struct Clue(pub [u8; 68]); +pub struct Clue(pub(crate) [u8; 68]); impl Clue { - /// The bits of precision for this `Clue`. - pub fn precision_bits(&self) -> u8 { - self.0[64] + /// The bits of precision for this `Clue`, if valid. + pub fn precision(&self) -> Result { + self.0[64].try_into() + } +} + +impl From for Vec { + fn from(value: Clue) -> Self { + value.0.into() + } +} + +impl TryFrom<&[u8]> for Clue { + type Error = TryFromSliceError; + + fn try_from(value: &[u8]) -> Result { + Ok(Self(value.try_into()?)) } } diff --git a/crates/crypto/decaf377-fmd/src/clue_key.rs b/crates/crypto/decaf377-fmd/src/clue_key.rs index ea33badfcc..17cc4b2312 100644 --- a/crates/crypto/decaf377-fmd/src/clue_key.rs +++ b/crates/crypto/decaf377-fmd/src/clue_key.rs @@ -5,7 +5,7 @@ use bitvec::{array::BitArray, order}; use decaf377::{FieldExt, Fq, Fr}; use rand_core::{CryptoRng, RngCore}; -use crate::{hash, hkd, Clue, Error, MAX_PRECISION}; +use crate::{hash, hkd, Clue, Error, Precision}; /// Bytes representing a clue key corresponding to some /// [`DetectionKey`](crate::DetectionKey). @@ -68,10 +68,6 @@ impl ExpandedClueKey { /// Checks that the expanded clue key has at least `precision` subkeys fn ensure_at_least(&self, precision: usize) -> Result<(), Error> { - if precision > MAX_PRECISION { - return Err(Error::PrecisionTooLarge(precision)); - } - let current_precision = self.subkeys.borrow().len(); // The cached expansion is large enough to accommodate the specified precision. @@ -101,13 +97,10 @@ impl ExpandedClueKey { #[allow(non_snake_case)] pub fn create_clue_deterministic( &self, - precision_bits: usize, + precision: Precision, rseed: [u8; 32], ) -> Result { - if precision_bits >= MAX_PRECISION { - return Err(Error::PrecisionTooLarge(precision_bits)); - } - + let precision_bits = precision.bits() as usize; // Ensure that at least `precision_bits` subkeys are available. self.ensure_at_least(precision_bits)?; @@ -171,12 +164,12 @@ impl ExpandedClueKey { #[allow(non_snake_case)] pub fn create_clue( &self, - precision_bits: usize, + precision: Precision, mut rng: R, ) -> Result { let mut rseed = [0u8; 32]; rng.fill_bytes(&mut rseed); - self.create_clue_deterministic(precision_bits, rseed) + self.create_clue_deterministic(precision, rseed) } } diff --git a/crates/crypto/decaf377-fmd/src/detection.rs b/crates/crypto/decaf377-fmd/src/detection.rs index d619cd2d2e..a5fe876261 100644 --- a/crates/crypto/decaf377-fmd/src/detection.rs +++ b/crates/crypto/decaf377-fmd/src/detection.rs @@ -16,7 +16,7 @@ pub struct DetectionKey { /// The detection key. dtk: Fr, /// Cached copies of the child detection keys; these can be fully derived from `dtk`. - xs: [Fr; MAX_PRECISION], + xs: [Fr; MAX_PRECISION as usize], } impl DetectionKey { @@ -36,7 +36,7 @@ impl DetectionKey { let root_pub = dtk * decaf377::basepoint(); let root_pub_enc = root_pub.vartime_compress(); - let xs: [_; MAX_PRECISION] = (0..MAX_PRECISION) + let xs: [_; MAX_PRECISION as usize] = (0..MAX_PRECISION as usize) .map(|i| { hkd::derive_private( &dtk, @@ -102,7 +102,10 @@ impl DetectionKey { return false; } - let precision_bits = clue.0[64]; + let precision_bits = match clue.precision() { + Err(_) => return false, + Ok(x) => x.bits() as u8, + }; let ciphertexts = BitSlice::::from_slice(&clue.0[65..68]); let m = hash::to_scalar(&P_encoding.0, precision_bits, &clue.0[65..68]); diff --git a/crates/crypto/decaf377-fmd/src/error.rs b/crates/crypto/decaf377-fmd/src/error.rs index 14cc3a6aee..f22a51e6df 100644 --- a/crates/crypto/decaf377-fmd/src/error.rs +++ b/crates/crypto/decaf377-fmd/src/error.rs @@ -5,7 +5,7 @@ use thiserror::Error; pub enum Error { /// Clue creation for larger than maximum precision was requested. #[error("Precision {0} is larger than `MAX_PRECISION` or current key expansion.")] - PrecisionTooLarge(usize), + PrecisionTooLarge(u64), /// An address encoding was invalid. #[error("Invalid address.")] InvalidAddress, diff --git a/crates/crypto/decaf377-fmd/src/lib.rs b/crates/crypto/decaf377-fmd/src/lib.rs index 652751daeb..0114aa2105 100644 --- a/crates/crypto/decaf377-fmd/src/lib.rs +++ b/crates/crypto/decaf377-fmd/src/lib.rs @@ -11,11 +11,13 @@ mod detection; mod error; mod hash; mod hkd; +mod precision; pub use clue::Clue; pub use clue_key::{ClueKey, ExpandedClueKey}; pub use detection::DetectionKey; pub use error::Error; +pub use precision::Precision; + +pub(crate) use precision::MAX_PRECISION; -/// The maximum detection precision, chosen so that the message bits fit in 3 bytes. -pub const MAX_PRECISION: usize = 24; diff --git a/crates/crypto/decaf377-fmd/src/precision.rs b/crates/crypto/decaf377-fmd/src/precision.rs new file mode 100644 index 0000000000..b0db357455 --- /dev/null +++ b/crates/crypto/decaf377-fmd/src/precision.rs @@ -0,0 +1,79 @@ +use core::fmt; + +use crate::Error; + +/// The maximum detection precision, chosen so that the message bits fit in 3 bytes. +pub(crate) const MAX_PRECISION: u8 = 24; + +/// Represents the precision governing the false positive rate of detection. +/// +/// This is usually measured in bits, where a precision of `n` bits yields false +/// positives with a rate of `2^-n`. +/// +/// This type implements `TryFrom` for `u8`, `u32`, `u64`, and `i32`, which has the behavior of considering +/// the value as a number of bits, and converting if this number isn't too large. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Precision(u8); + +impl Precision { + pub fn new(precision_bits: u8) -> Result { + if precision_bits > MAX_PRECISION { + return Err(Error::PrecisionTooLarge(precision_bits.into())); + } + Ok(Self(precision_bits)) + } + + pub fn bits(&self) -> u8 { + self.0 + } +} + +impl Default for Precision { + fn default() -> Self { + Self(0) + } +} + +impl fmt::Display for Precision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl TryFrom for Precision { + type Error = Error; + + fn try_from(value: u8) -> Result { + Self::new(value) + } +} + +impl TryFrom for Precision { + type Error = Error; + + fn try_from(value: u32) -> Result { + u8::try_from(value) + .map_err(|_| Error::PrecisionTooLarge(value.into()))? + .try_into() + } +} + +impl TryFrom for Precision { + type Error = Error; + + fn try_from(value: u64) -> Result { + u8::try_from(value) + .map_err(|_| Error::PrecisionTooLarge(value))? + .try_into() + } +} + +impl TryFrom for Precision { + type Error = Error; + + fn try_from(value: i32) -> Result { + u8::try_from(value) + .map_err(|_| Error::PrecisionTooLarge(value as u64))? + .try_into() + } +} diff --git a/crates/crypto/decaf377-fmd/tests/fmd.rs b/crates/crypto/decaf377-fmd/tests/fmd.rs index e2ded6a82f..513e2b471b 100644 --- a/crates/crypto/decaf377-fmd/tests/fmd.rs +++ b/crates/crypto/decaf377-fmd/tests/fmd.rs @@ -1,5 +1,5 @@ use decaf377_fmd as fmd; -use fmd::ClueKey; +use fmd::{ClueKey, Precision}; use rand_core::OsRng; #[test] @@ -10,10 +10,14 @@ fn detection_distribution_matches_expectation() { let bobce_dk = fmd::DetectionKey::new(OsRng); const NUM_CLUES: usize = 1024; - const PRECISION_BITS: usize = 4; // p = 1/16 + const PRECISION_BITS: u8 = 4; // p = 1/16 let clues = (0..NUM_CLUES) - .map(|_| alice_clue_key.create_clue(PRECISION_BITS, OsRng).unwrap()) + .map(|_| { + alice_clue_key + .create_clue(Precision::new(PRECISION_BITS).unwrap(), OsRng) + .unwrap() + }) .collect::>(); let alice_detections = clues.iter().filter(|clue| alice_dk.examine(clue)).count(); @@ -43,10 +47,5 @@ fn fails_to_expand_clue_key() { #[test] fn fails_to_generate_clue() { - let detection_key = fmd::DetectionKey::new(OsRng); - let expanded_clue_key = detection_key.clue_key().expand().unwrap(); - - expanded_clue_key - .create_clue(fmd::MAX_PRECISION + 1, OsRng) - .expect_err("fails to generate clue with excessive precision"); + Precision::new(25).expect_err("fails to generate clue with excessive precision"); } diff --git a/crates/proto/src/protobuf.rs b/crates/proto/src/protobuf.rs index ce074423fb..4b9d127f33 100644 --- a/crates/proto/src/protobuf.rs +++ b/crates/proto/src/protobuf.rs @@ -109,9 +109,7 @@ impl DomainType for Clue { impl From for ProtoClue { fn from(msg: Clue) -> Self { - ProtoClue { - inner: bytes::Bytes::copy_from_slice(&msg.0).to_vec(), - } + ProtoClue { inner: msg.into() } } } @@ -119,11 +117,9 @@ impl TryFrom for Clue { type Error = anyhow::Error; fn try_from(proto: ProtoClue) -> Result { - let clue: [u8; 68] = proto.inner[..] + proto.inner[..] .try_into() - .map_err(|_| anyhow::anyhow!("expected 68-byte clue"))?; - - Ok(Clue(clue)) + .map_err(|_| anyhow::anyhow!("expected 68-byte clue")) } } From d1a27f818676db95f7a1f2b7eceadcf98f028570 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Fri, 29 Mar 2024 10:07:46 -0700 Subject: [PATCH 02/14] Use separate personalization string for fmd scalar hash BREAKING: This will break old clues and their detection --- crates/crypto/decaf377-fmd/src/hash.rs | 2 +- crates/crypto/decaf377-fmd/src/lib.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/crypto/decaf377-fmd/src/hash.rs b/crates/crypto/decaf377-fmd/src/hash.rs index d8567ceaaa..d613e3d99a 100644 --- a/crates/crypto/decaf377-fmd/src/hash.rs +++ b/crates/crypto/decaf377-fmd/src/hash.rs @@ -18,7 +18,7 @@ pub fn to_scalar(point: &[u8; 32], n: u8, bits: &[u8]) -> Fr { assert_eq!(bits.len(), 3); let hash = blake2b_simd::Params::default() - .personal(b"decaf377-fmd.bit") + .personal(b"decaf377-fmd.sca") .to_state() .update(point) .update(&[n]) diff --git a/crates/crypto/decaf377-fmd/src/lib.rs b/crates/crypto/decaf377-fmd/src/lib.rs index 0114aa2105..d54c58b2cb 100644 --- a/crates/crypto/decaf377-fmd/src/lib.rs +++ b/crates/crypto/decaf377-fmd/src/lib.rs @@ -20,4 +20,3 @@ pub use error::Error; pub use precision::Precision; pub(crate) use precision::MAX_PRECISION; - From b38db3f3f5d478d0e21544f3790df14706f751d0 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Fri, 29 Mar 2024 11:26:48 -0700 Subject: [PATCH 03/14] Define FMD Meta Parameters BREAKING: requires migration because we broke protobufs for shielded pool params. --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 99467 -> 99489 bytes crates/core/app/src/params/change.rs | 10 +- .../core/component/shielded-pool/src/fmd.rs | 43 +++++++ .../component/shielded-pool/src/params.rs | 12 +- ...enumbra.core.component.shielded_pool.v1.rs | 27 ++++ ...a.core.component.shielded_pool.v1.serde.rs | 118 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 481372 -> 481785 bytes .../shielded_pool/v1/shielded_pool.proto | 9 +- 8 files changed, 205 insertions(+), 14 deletions(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index cb5f8b9377fc15a61da07be47f5ef6ad339b0300..6e23fc5c3fa90b2ae6af49ab1a84a133ac4ac9bb 100644 GIT binary patch delta 20407 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 diff --git a/crates/core/app/src/params/change.rs b/crates/core/app/src/params/change.rs index e4a91053e1..44a3a897f0 100644 --- a/crates/core/app/src/params/change.rs +++ b/crates/core/app/src/params/change.rs @@ -90,10 +90,7 @@ impl AppParameters { outbound_ics20_transfers_enabled: _, }, sct_params: SctParameters { epoch_duration }, - shielded_pool_params: - ShieldedPoolParameters { - fixed_fmd_params: _, - }, + shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ }, stake_params: StakeParameters { active_validator_limit, @@ -188,10 +185,7 @@ impl AppParameters { outbound_ics20_transfers_enabled, }, sct_params: SctParameters { epoch_duration }, - shielded_pool_params: - ShieldedPoolParameters { - fixed_fmd_params: _, - }, + shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ }, stake_params: StakeParameters { active_validator_limit, diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index c363520ae4..0d0ac6671f 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use decaf377_fmd::Precision; use penumbra_proto::{core::component::shielded_pool::v1 as pb, DomainType}; use serde::{Deserialize, Serialize}; @@ -45,3 +46,45 @@ impl Default for Parameters { } } } + +/// Meta parameters are an algorithm for dynamically choosing FMD parameters. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "pb::FmdMetaParameters", into = "pb::FmdMetaParameters")] +pub enum MetaParameters { + /// Used a fixed precision forever. + Fixed(Precision), +} + +impl TryFrom for MetaParameters { + type Error = anyhow::Error; + + fn try_from(value: pb::FmdMetaParameters) -> Result { + match value.algorithm.ok_or(anyhow!("missing algorithm"))? { + pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits(p) => { + Ok(MetaParameters::Fixed(Precision::new(p as u8)?)) + } + } + } +} + +impl From for pb::FmdMetaParameters { + fn from(value: MetaParameters) -> Self { + match value { + MetaParameters::Fixed(p) => pb::FmdMetaParameters { + algorithm: Some(pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits( + p.bits().into(), + )), + }, + } + } +} + +impl DomainType for MetaParameters { + type Proto = pb::FmdMetaParameters; +} + +impl Default for MetaParameters { + fn default() -> Self { + Self::Fixed(Precision::default()) + } +} diff --git a/crates/core/component/shielded-pool/src/params.rs b/crates/core/component/shielded-pool/src/params.rs index 8c59d0c40c..6658420fcc 100644 --- a/crates/core/component/shielded-pool/src/params.rs +++ b/crates/core/component/shielded-pool/src/params.rs @@ -11,7 +11,7 @@ use crate::fmd; into = "pb::ShieldedPoolParameters" )] pub struct ShieldedPoolParameters { - pub fixed_fmd_params: fmd::Parameters, + pub fmd_meta_params: fmd::MetaParameters, } impl DomainType for ShieldedPoolParameters { @@ -23,9 +23,9 @@ impl TryFrom for ShieldedPoolParameters { fn try_from(msg: pb::ShieldedPoolParameters) -> anyhow::Result { Ok(ShieldedPoolParameters { - fixed_fmd_params: msg - .fixed_fmd_params - .ok_or_else(|| anyhow::anyhow!("missing fmd_parameters"))? + fmd_meta_params: msg + .fmd_meta_params + .ok_or_else(|| anyhow::anyhow!("missing fmd_meta_params"))? .try_into()?, }) } @@ -33,8 +33,10 @@ impl TryFrom for ShieldedPoolParameters { impl From for pb::ShieldedPoolParameters { fn from(params: ShieldedPoolParameters) -> Self { + #[allow(deprecated)] pb::ShieldedPoolParameters { - fixed_fmd_params: Some(params.fixed_fmd_params.into()), + fmd_meta_params: Some(params.fmd_meta_params.into()), + fixed_fmd_params: None, } } } diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index b2c3ff37c5..c5bab63c7f 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -2,8 +2,11 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ShieldedPoolParameters { + #[deprecated] #[prost(message, optional, tag = "1")] pub fixed_fmd_params: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub fmd_meta_params: ::core::option::Option, } impl ::prost::Name for ShieldedPoolParameters { const NAME: &'static str = "ShieldedPoolParameters"; @@ -57,6 +60,30 @@ impl ::prost::Name for GenesisContent { ) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FmdMetaParameters { + #[prost(oneof = "fmd_meta_parameters::Algorithm", tags = "1")] + pub algorithm: ::core::option::Option, +} +/// Nested message and enum types in `FmdMetaParameters`. +pub mod fmd_meta_parameters { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Algorithm { + #[prost(uint32, tag = "1")] + FixedPrecisionBits(u32), + } +} +impl ::prost::Name for FmdMetaParameters { + const NAME: &'static str = "FmdMetaParameters"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} /// Parameters for Fuzzy Message Detection #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index 67ce5950df..bdb64105f6 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -573,6 +573,106 @@ impl<'de> serde::Deserialize<'de> for EventSpend { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventSpend", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for FmdMetaParameters { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.algorithm.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.FmdMetaParameters", len)?; + if let Some(v) = self.algorithm.as_ref() { + match v { + fmd_meta_parameters::Algorithm::FixedPrecisionBits(v) => { + struct_ser.serialize_field("fixedPrecisionBits", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FmdMetaParameters { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "fixed_precision_bits", + "fixedPrecisionBits", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + FixedPrecisionBits, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "fixedPrecisionBits" | "fixed_precision_bits" => Ok(GeneratedField::FixedPrecisionBits), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FmdMetaParameters; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.FmdMetaParameters") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut algorithm__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::FixedPrecisionBits => { + if algorithm__.is_some() { + return Err(serde::de::Error::duplicate_field("fixedPrecisionBits")); + } + algorithm__ = map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| fmd_meta_parameters::Algorithm::FixedPrecisionBits(x.0)); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(FmdMetaParameters { + algorithm: algorithm__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.FmdMetaParameters", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for FmdParameters { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -2244,10 +2344,16 @@ impl serde::Serialize for ShieldedPoolParameters { if self.fixed_fmd_params.is_some() { len += 1; } + if self.fmd_meta_params.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.ShieldedPoolParameters", len)?; if let Some(v) = self.fixed_fmd_params.as_ref() { struct_ser.serialize_field("fixedFmdParams", v)?; } + if let Some(v) = self.fmd_meta_params.as_ref() { + struct_ser.serialize_field("fmdMetaParams", v)?; + } struct_ser.end() } } @@ -2260,11 +2366,14 @@ impl<'de> serde::Deserialize<'de> for ShieldedPoolParameters { const FIELDS: &[&str] = &[ "fixed_fmd_params", "fixedFmdParams", + "fmd_meta_params", + "fmdMetaParams", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { FixedFmdParams, + FmdMetaParams, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -2288,6 +2397,7 @@ impl<'de> serde::Deserialize<'de> for ShieldedPoolParameters { { match value { "fixedFmdParams" | "fixed_fmd_params" => Ok(GeneratedField::FixedFmdParams), + "fmdMetaParams" | "fmd_meta_params" => Ok(GeneratedField::FmdMetaParams), _ => Ok(GeneratedField::__SkipField__), } } @@ -2308,6 +2418,7 @@ impl<'de> serde::Deserialize<'de> for ShieldedPoolParameters { V: serde::de::MapAccess<'de>, { let mut fixed_fmd_params__ = None; + let mut fmd_meta_params__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::FixedFmdParams => { @@ -2316,6 +2427,12 @@ impl<'de> serde::Deserialize<'de> for ShieldedPoolParameters { } fixed_fmd_params__ = map_.next_value()?; } + GeneratedField::FmdMetaParams => { + if fmd_meta_params__.is_some() { + return Err(serde::de::Error::duplicate_field("fmdMetaParams")); + } + fmd_meta_params__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -2323,6 +2440,7 @@ impl<'de> serde::Deserialize<'de> for ShieldedPoolParameters { } Ok(ShieldedPoolParameters { fixed_fmd_params: fixed_fmd_params__, + fmd_meta_params: fmd_meta_params__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 6c72f839f9e5ed8c07f1477d7b062a002404e36a..0cce30a0dec79b61d0bdefd298e3ce11e17fc0a5 100644 GIT binary patch delta 25527 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 bxJjVJPt18`rRJZ Date: Fri, 29 Mar 2024 13:49:58 -0700 Subject: [PATCH 04/14] Add mechanism for using meta params to update fmd params periodically --- .../src/component/shielded_pool.rs | 26 +++++++++++++++--- .../core/component/shielded-pool/src/fmd.rs | 27 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index 8e7ebaab6e..82e81385dd 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::fmd::should_update_fmd_params; use crate::params::ShieldedPoolParameters; use crate::{fmd, genesis, state_key}; use anyhow::anyhow; @@ -62,11 +63,30 @@ impl Component for ShieldedPool { ) { } - #[instrument(name = "shielded_pool", skip(_state, _end_block))] + #[instrument(name = "shielded_pool", skip(state, end_block))] async fn end_block( - _state: &mut Arc, - _end_block: &abci::request::EndBlock, + state: &mut Arc, + end_block: &abci::request::EndBlock, ) { + let height: u64 = end_block + .height + .try_into() + .expect("height should not be negative"); + if should_update_fmd_params(height) { + let state = Arc::get_mut(state).expect("the state should not be shared"); + let meta_params = state + .get_shielded_pool_params() + .await + .expect("should be able to read state") + .fmd_meta_params; + let old = state + .get_current_fmd_parameters() + .await + .expect("should be able to read state"); + let new = meta_params.updated_fmd_params(&old, height); + state.put_previous_fmd_parameters(old); + state.put_current_fmd_parameters(new); + } } async fn end_epoch(mut _state: &mut Arc) -> Result<()> { diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index 0d0ac6671f..e8e1541949 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,10 +1,24 @@ use anyhow::anyhow; use decaf377_fmd::Precision; -use penumbra_proto::{core::component::shielded_pool::v1 as pb, DomainType}; +use penumbra_proto::{ + core::component::shielded_pool::v1::{self as pb}, + DomainType, +}; use serde::{Deserialize, Serialize}; pub mod state_key; +/// How long users have to switch to updated parameters. +pub const FMD_GRACE_PERIOD_BLOCKS: u64 = 1 << 4; +/// How often we update the params. +pub const FMD_UPDATE_FREQUENCY_BLOCKS: u64 = 1 << 6; +/// How many blocks we expect per day, approximately. +const _BLOCKS_PER_DAY: u64 = 1 << 13; + +pub fn should_update_fmd_params(height: u64) -> bool { + height % FMD_UPDATE_FREQUENCY_BLOCKS == 0 +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "pb::FmdParameters", into = "pb::FmdParameters")] pub struct Parameters { @@ -88,3 +102,14 @@ impl Default for MetaParameters { Self::Fixed(Precision::default()) } } + +impl MetaParameters { + pub fn updated_fmd_params(&self, _old: &Parameters, height: u64) -> Parameters { + match *self { + MetaParameters::Fixed(precision) => Parameters { + precision, + as_of_block_height: height, + }, + } + } +} From 8570ca0856f8db1f6673e83d660e8dd71fe7da5d Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 2 Apr 2024 15:04:42 -0700 Subject: [PATCH 05/14] Add extension traits for managing clues --- .../src/component/shielded_pool.rs | 8 +- .../core/component/shielded-pool/src/fmd.rs | 96 ++++++++++++++- .../shielded-pool/src/fmd/state_key.rs | 10 ++ .../core/component/shielded-pool/src/lib.rs | 1 + ...enumbra.core.component.shielded_pool.v1.rs | 20 ++++ ...a.core.component.shielded_pool.v1.serde.rs | 112 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 481785 -> 482213 bytes .../shielded_pool/v1/shielded_pool.proto | 8 ++ 8 files changed, 247 insertions(+), 8 deletions(-) diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index 82e81385dd..c00e687a75 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::fmd::should_update_fmd_params; +use crate::fmd::{should_update_fmd_params, ClueManagerInternal as _}; use crate::params::ShieldedPoolParameters; use crate::{fmd, genesis, state_key}; use anyhow::anyhow; @@ -83,7 +83,11 @@ impl Component for ShieldedPool { .get_current_fmd_parameters() .await .expect("should be able to read state"); - let new = meta_params.updated_fmd_params(&old, height); + let clue_count_delta = state + .flush_clue_count() + .await + .expect("should be able to read state"); + let new = meta_params.updated_fmd_params(&old, height, clue_count_delta); state.put_previous_fmd_parameters(old); state.put_current_fmd_parameters(new); } diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index e8e1541949..e500048aa6 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; -use decaf377_fmd::Precision; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use decaf377_fmd::{Clue, Precision}; use penumbra_proto::{ core::component::shielded_pool::v1::{self as pb}, - DomainType, + DomainType, StateWriteProto, }; +use penumbra_txhash::TransactionId; use serde::{Deserialize, Serialize}; pub mod state_key; @@ -35,7 +38,7 @@ impl DomainType for Parameters { impl TryFrom for Parameters { type Error = anyhow::Error; - fn try_from(msg: pb::FmdParameters) -> Result { + fn try_from(msg: pb::FmdParameters) -> Result { Ok(Parameters { precision: msg.precision_bits.try_into()?, as_of_block_height: msg.as_of_block_height, @@ -72,7 +75,7 @@ pub enum MetaParameters { impl TryFrom for MetaParameters { type Error = anyhow::Error; - fn try_from(value: pb::FmdMetaParameters) -> Result { + fn try_from(value: pb::FmdMetaParameters) -> Result { match value.algorithm.ok_or(anyhow!("missing algorithm"))? { pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits(p) => { Ok(MetaParameters::Fixed(Precision::new(p as u8)?)) @@ -104,7 +107,7 @@ impl Default for MetaParameters { } impl MetaParameters { - pub fn updated_fmd_params(&self, _old: &Parameters, height: u64) -> Parameters { + pub fn updated_fmd_params(&self, _old: &Parameters, height: u64, _clue_count_delta: (u64, u64)) -> Parameters { match *self { MetaParameters::Fixed(precision) => Parameters { precision, @@ -113,3 +116,84 @@ impl MetaParameters { } } } + +#[async_trait] +trait ClueWriteExt: StateWrite { + fn put_current_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::current().to_string(), + count.to_be_bytes().to_vec(), + ) + } + + fn put_previous_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::previous().to_string(), + count.to_be_bytes().to_vec(), + ) + } +} + +impl ClueWriteExt for T {} + +#[async_trait] +trait ClueReadExt: StateRead { + async fn get_current_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::current()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } + + async fn get_previous_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::previous()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } +} + +impl ClueReadExt for T {} + +#[async_trait] +pub trait ClueManager: StateRead + StateWrite { + async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { + // Update count + { + let count = self.get_current_clue_count().await?; + self.put_current_clue_count(count.saturating_add(1)); + } + self.record_proto(pb::EventClue { + clue: Some(clue.into()), + tx: Some(tx.into()) + }); + Ok(()) + } +} + +impl ClueManager for T {} + +#[async_trait] +pub(crate) trait ClueManagerInternal: ClueManager { + fn init(&mut self) { + self.put_current_clue_count(0); + self.put_previous_clue_count(0); + } + + /// Flush the clue counts, returning the previous and current counts + async fn flush_clue_count(&mut self) -> Result<(u64, u64)> { + let previous = self.get_previous_clue_count().await?; + let current = self.get_current_clue_count().await?; + self.put_previous_clue_count(current); + self.put_current_clue_count(0); + Ok((previous, current)) + } +} + +impl ClueManagerInternal for T {} diff --git a/crates/core/component/shielded-pool/src/fmd/state_key.rs b/crates/core/component/shielded-pool/src/fmd/state_key.rs index d1e47596f7..4a3d1fcc27 100644 --- a/crates/core/component/shielded-pool/src/fmd/state_key.rs +++ b/crates/core/component/shielded-pool/src/fmd/state_key.rs @@ -9,3 +9,13 @@ pub mod parameters { "shielded_pool/fmd_parameters/previous" } } + +pub(super) mod clue_count { + pub fn current() -> &'static str { + "shielded_pool/fmd_clue_count/current" + } + + pub fn previous() -> &'static str { + "shielded_pool/fmd_clue_count/previous" + } +} diff --git a/crates/core/component/shielded-pool/src/lib.rs b/crates/core/component/shielded-pool/src/lib.rs index 84d112ed52..96c97789df 100644 --- a/crates/core/component/shielded-pool/src/lib.rs +++ b/crates/core/component/shielded-pool/src/lib.rs @@ -26,6 +26,7 @@ pub mod output; pub mod spend; pub use convert::{ConvertCircuit, ConvertProof, ConvertProofPrivate, ConvertProofPublic}; +pub use fmd::ClueManager; pub use nullifier_derivation::{ NullifierDerivationCircuit, NullifierDerivationProof, NullifierDerivationProofPrivate, NullifierDerivationProofPublic, diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index c5bab63c7f..dd2ece1b63 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -291,6 +291,26 @@ impl ::prost::Name for EventOutput { ) } } +/// ABCI Event recording a clue. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventClue { + #[prost(message, optional, tag = "1")] + pub clue: ::core::option::Option< + super::super::super::super::crypto::decaf377_fmd::v1::Clue, + >, + #[prost(message, optional, tag = "2")] + pub tx: ::core::option::Option, +} +impl ::prost::Name for EventClue { + const NAME: &'static str = "EventClue"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} /// The body of a spend description, containing only the effecting data /// describing changes to the ledger, and not the authorizing data that allows /// those changes to be performed. diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index bdb64105f6..167ca4f8a6 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -382,6 +382,118 @@ impl<'de> serde::Deserialize<'de> for AssetMetadataByIdsResponse { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.AssetMetadataByIdsResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventClue { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.clue.is_some() { + len += 1; + } + if self.tx.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", len)?; + if let Some(v) = self.clue.as_ref() { + struct_ser.serialize_field("clue", v)?; + } + if let Some(v) = self.tx.as_ref() { + struct_ser.serialize_field("tx", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventClue { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "clue", + "tx", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Clue, + Tx, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "clue" => Ok(GeneratedField::Clue), + "tx" => Ok(GeneratedField::Tx), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventClue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventClue") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut clue__ = None; + let mut tx__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Clue => { + if clue__.is_some() { + return Err(serde::de::Error::duplicate_field("clue")); + } + clue__ = map_.next_value()?; + } + GeneratedField::Tx => { + if tx__.is_some() { + return Err(serde::de::Error::duplicate_field("tx")); + } + tx__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventClue { + clue: clue__, + tx: tx__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for EventOutput { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 0cce30a0dec79b61d0bdefd298e3ce11e17fc0a5..731fe8752f868a4fe90b797163c47ad7dbda099b 100644 GIT binary patch delta 5134 zcmY*ddyG`o8Nc7TXYS74*`3Fo$IcG>m|6D4K89s?7FeLdb_EqwP{G6ye6h69;I2|A zMa1DDVpZIlBMxeC#TO!$f~%tS6-x;r1dQ~LmL>*cQkvS(SZJCwHfev~x%V>m55oPO z@B4n=<9z2kXZGI{^{*YPKl19v332#3aqOwHW*tquy73ChXV%(tN;D|brL)fkn$;gR zeW=EcwwCCe`Q*em4?b2=_pn_#|7WX4)(@@Su*w;*?Mhpi z0mlp)bM&X>n3cvg`m@3@ZKJ~;w1qSrS#3ns+@}VM)xickWJ_rX!!g5Rz^<|b3}6@* z5g!pyH0vW1XmYcUfkllfW4faUZcPQ9s#-u#2>|*|QNI zIWlh4Q)X)EtNoNxcRtmimW;__Jebn7#GxicueJmLZ9=4d1lmMxw~xS@=$Qs2C7DP& zDQZnuW*X2;+6f&vWK5cpop#u+U{y_*EHEKz?bRBR%42~{^>%rHJH>?CR4SEI(1Fkq+*S> z!4hoRGLt+7(6tM<`%J-gZ5KgLkyuta$wx42Z)ntMNk?`XQ{A-CQD$ebO4CX9*CkvN z092_d?DlDHrN6x@t3?9Z0%`u!mg0b*=w<*X00>O8ya<56Gz$?t2&M{0&NZ&1%!umQ->eosb6a?B zFphyGWd#!sszc8Vbqor(9#{g=V0Nkw2?#`ky?vfKL_;dQXQ*??^IM@aA41b{v4NmI zl&-aRkfulK3)G`~c6KcAdZ9vRyddT~6VP9PE}nuogN|Hi+^G7Woicx+*Tq2&aFLfW z05V?Wc?Lo>i@c11z_bV%w}YwDk&BI6)GN=n&tDu&=sv4trX?N|0GO6|Oh8~-;xPe% zX$hESfXQ~`QsZ_NEzOv}G}xq>Y-U>KF#&*Sna2bKrez)z5SW&MsRc|SN8VxFt)48k zgztdDRMaQLOv}}$rD+|@MMzf=OJG=D<(?cMFf7M7o1xVqTA?y~2Rc@WE>E2sZ$)>% zkD$I{=77COtE2jNsV#eN>bOgUJq7N!yCQD0hj282YSMNm;;VAxN@JD!=ibhnRt6oN zX%)veEPAvp09togxU&ujOv6o`z6?ym`Kfj{n5rGQ+V}}&a@oNj4&9(0Se;N?pNkb& zqv`lctY+3VqM$7Sz`Dk3J|M8J@tO|^tZUGG9bgSR@;>8!$~0)!tiJ8D>sGBCT{paD z2L(sYbnY z0bn*N+?fKzvP_*v$xT1nn6*D4#2Inq2IC>h9Hs2i)vL1ijt+0ij{YcnkNWF#Z=@oy z+kn7kqYe=cY@;Y-@@m`5P3qM1u^ueJ%|_u813`Uby?Yda;ASJL!L1YF=r)y>KW=uy zii6wabtDjr;ywolw+X@BrmfHtn@jIKZZ0lH1dpQ4BCnGGg4O1!e5VJ%&YiKj(2q(8 z8Fd0%j9-vD^P^DPf()ub7sb^dcvx&B_bvm3@L`*U77IDSqD<(!RYN!|%1VukdI9=H zJ5pAF*P;^#cJ1+E*-A16jS9_&Jb9t2yr4CzW90>0geoM;9>{qDB%LPxwVtTW4;T%&*e_S3F6B> z%KVR#OuPPhdl`o8>LSO(8w^|(_r;)olu~?Mvn_I|8e-6Vg0N==u*Nu|-6YiW`|^DNAO*5C3m{Z?Qw8D!DIiF9Q#fMJ z(v}d8sqgkppBdxZPmU4Z3$7y9b&M+AU?7Fv7zg8e#8c>L5;d9DQs7{=Rn9b_j7i$nZ9^}00Ghnj;aaFnurnbQ= z<9f}2cgr3%Yy4W+N!aZv+wr@24+Y&Vf)sXpXi}YhgSLZiNgW-ZUx$MHB6U*odh3dT4GcR+o&|K>h?B9a3nsx<{56c121<3lhWAiRvO({>OZ zR1dv81UqiigM<;rkpQxYFvC0uyMu%whTmwAqB9Pu%P;pA@QLak@~aC7yF-3;0bzHD ztGkUK7IB7$)$&)i6!8he!(~HmgToZ8@f>m+945SZ`CKF%>nItYdgmem&7+(<&x!A)4Ds89X~xqCkc%_nPa}79u0a0CKH4+$48u zen@x$@MKGHDL?Y3A`lV)bf*p=$UpLjA|RMQ@`qxAbNiTM<9%Fk!P)-njg}90w$_a8BdpToaAW|^_kgf?s&KQ z+krSj=aGm-pMXc=Y5(v+3WC#o_!epc=$=v2-dIpb>XCS+Y{w(7 z#1~}zNgaA)-Ys7cUghWnOCPFpByQ(q8G+y&S^DrYM3v4_y2qD-?;OouYX4qnTPU9= zad+l-qO|RbOikQ+aDWESlch&F%n9b_skYZM=lwZPKN#{M*v`|nciMB%8!5;7l8mp2 zGWk-^pqVPQ%(3bdDR}sjf?56iPH~T1pwjJgtllL6V7q`@joBn1R4-6E>qC%VApO@I zA!{7#Ycl@gHC_!gzouZ$-pQk^hFLFC>5p@)tuq0@dXeyF7iI;7?nSC?^C4I-Qb(t~ z03oLx>l-ruj*w?w4LPFLy_HU+Vf77r7-tV@_HaqP_|^+6@QK~OMEGL{^8>=&B?`Gw z1H$kU<)-*jxVuF7V+fy~TF3g9j4NIewb1;Q@QY|JJk+v>%j(%*&2GmhC@=dN07CJy rp8+5!FC&8;^N#GAcZBdy9GiE?vlGB0FV|0H!{GNrnHGc@}*?XX?ZeY#V9+NTT-a( zva{;5^Vz)X+R`G=jyQF0#Ff$#mS;!Bd^hTb7=#&B1_+MPcrHLT(43|KIY7fW^ff2llmbvai1WcYxShLjAYGFR%WV94 zXF59FUQSIwJgct%wm+X0uF0KCoQzSa`D5Y9Kt@l67jG8hA-5 z9Mr2$|wScfzed8Q=0BhNvY_jI6 zBZmj_O-@=%*(}v8W@~Q%NHq&5Ku~HfuL=;9nzOSE4W<_1_n|Gg+AT#Iw^K{`EJGvE zTI%X`veJ{SRwt6pBU?0=-ReZOp5!pvgwFv$@!CXOD*(Zb?d-@AT-j~Z`@%TRX=@YN&Bc-w@jx$iYDb<0D}8yb&b29v^?@3Q-6QuK--vU zk}{q5Si;`|Ao!1=>st}04)c>-8AhZfoa7eRYTCFx6o!_=vbQpJT!quIF0fPJL;rZ@E+8vv) zM%_I6aOWBkGY;H*YbyQDkLXQUQ&sJ@Bh08L*IG|fwqA9-R=sB1mi3cA>cTgLF%Si3 zt>ft$L^<+xqDRXCptIKre`*53ab073U_&hHy1Ly?h*x;>DQhET8*-!HT{5AzZcM3< zk0l;>3hl@TY6Yuq5CfVC0M!kqErFoA!L%h1R5ze4|45LHd2*ArUEO@`4_*vjn=pMB zLN3N~o7K_dLp^we*k&_6f#9~e*6(p3h;4=#?@5eHvsKxzw}ZrI!&bA?Kx|3+6Br1j zt@Sx~g(jiK+td@UFVDvWf9Bgnmreo*q;1`Nw1%L~?Y?bb5ZMY@>4kP!&yqi-D&e*R zar22%$tUnL;^+Q}0tDMLZbF}+K%CW1nbIemhFEW>EcbDq?eO2}R+b#F=uR(L-;xFoyXxRJ&sBk$RJim~~Zjy*waHZc76 z6hi}s_8uJ?v~iGz_KIJU5!WZ_Uej=uob^8GUta(a^FHZ+D?m`)7uC0@a6Idg*Mf~FgHqi1qPa)%8ct2Uc#^yxO`Eew@pRhXvC_x;N$O9ze z*_PbOjq7t;o?iRo$?fZRZO=_^-o9R4`(5=gTrk7o6GmY|%z(fFDvJaV=lcPQRk{nb z9*hS`WU?KF-pWu~HBHU#kMkMBZyiHHp#jUe30M%Z0wP#HBHT@$SKFp~?vD!r-t&Zc zYQy7e%^{jK+nvxn_`an6baH$kE<*fV;^D{!Eb>C& zy~Ml@d^HI#tjKy<%|A7_IsrdLPR#9tV1oDMFxRoE(0S0Fy?FDE?OS$i$ME8)Va1lf z91S42hY4+NtXShP7j#fd!B|kAoa!A+aLdR7;a=}M@>*1;T;%XC6eJ7d}ST z8MWZ_)uacp~af)RF2*u-6TjzdH^I$xoUOck| zJdWf9VTN%efNUZRF$2LnL6~CmH4iN@rRa^p0Z+HYR8U<&@TP+50)jWi)qR$aY>(k- z^^-UD=JAMaIb9-h8=NMm$}GrjaGG$>j3V=-7k-nh%jSbg!t+heJqF1n###6+5^v+f z=1_rQev2^CV)zFLkc)Gc@R3&Hp9+nUX&!If(i>3D628@<3syhN8y0B5;oP@1xLSA- zEi$-X=Xl2h3I2%AeOm%GJ5O@1@4%B3qnzKA0^{19r>e%H2AAtRHS@naoGE$X3nbnv zX3A?`APh)9N(?+tzFQL11Cn0^iRuZ77f3YkhpA^I-snuR1cqdIn#AveC3s58X~GH0 z^Et^2TqMj%U1{BFfd1ft5xy4*lhRlb{6)f~^r#JcjT&GC9f`k;9aGLMm+{nxO2;E>Z`L|_u~-_d@X2TK=7^+ zE?>S3@7*=R^^AvMiqQxEHw=gI9wIRK!w?W69|Xe?P}2}J5499Vfc%g|ZFZqrcCu6b z_goUu^HjvkK!B&>hr#y;D+E5|@9#k^0N?9s@%aY^Q+g_1FY$OPUN7-@Dqd$Eey<3l zDLx|WZ&bKBDQod#MdL#s5iY*i*RcM&ZjgA4vt|UD8x+=GF+-H=237Y2R;b>fg)7~U zh33L}W_tYXAJFIwg>_efL*UF%O`qZL?#$4lB>{xQ42?bE-jB{md*M&X`iv;sRrq?$ zu2EmTlTD_f`YAa%eVt8ngWOUL7y6gu5mL8E_#*}oj<={f$I22A%C|`Wj3Q*l3x7`5 z7pCVj@cf*d2KN9@u?%b8R#O+A9mXRxZxep`U=cug-lm$l0fgplYHN2NK(tj}_)D_> zfoO;QXm`BZl&S*ZOO`&)(p4;dN1cE7$QnFiyYCQwN?>U~taOJWez<@j-l2xpz>1ab z5Ppi_FQ(cH-zEG6 Date: Mon, 15 Apr 2024 12:56:25 -0700 Subject: [PATCH 06/14] Move FMD component to separate module This fixes the WASM build. --- .../component/shielded-pool/src/component.rs | 2 + .../shielded-pool/src/component/fmd.rs | 92 ++++++++++++++++++ .../src/component/shielded_pool.rs | 3 +- .../core/component/shielded-pool/src/fmd.rs | 95 ++----------------- .../shielded-pool/src/fmd/state_key.rs | 2 +- .../core/component/shielded-pool/src/lib.rs | 1 - 6 files changed, 105 insertions(+), 90 deletions(-) create mode 100644 crates/core/component/shielded-pool/src/component/fmd.rs diff --git a/crates/core/component/shielded-pool/src/component.rs b/crates/core/component/shielded-pool/src/component.rs index 7d69eaad59..2e89b08526 100644 --- a/crates/core/component/shielded-pool/src/component.rs +++ b/crates/core/component/shielded-pool/src/component.rs @@ -2,6 +2,7 @@ mod action_handler; mod assets; +mod fmd; mod metrics; mod note_manager; mod shielded_pool; @@ -9,6 +10,7 @@ mod transfer; pub use self::metrics::register_metrics; pub use assets::{AssetRegistry, AssetRegistryRead}; +pub use fmd::ClueManager; pub use note_manager::NoteManager; pub use shielded_pool::{ShieldedPool, StateReadExt, StateWriteExt}; pub use transfer::Ics20Transfer; diff --git a/crates/core/component/shielded-pool/src/component/fmd.rs b/crates/core/component/shielded-pool/src/component/fmd.rs new file mode 100644 index 0000000000..a9e42b15ba --- /dev/null +++ b/crates/core/component/shielded-pool/src/component/fmd.rs @@ -0,0 +1,92 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use decaf377_fmd::Clue; +use penumbra_proto::{ + core::component::shielded_pool::v1::{self as pb}, + StateWriteProto, +}; +use penumbra_txhash::TransactionId; + +use crate::fmd::state_key; + +#[async_trait] +trait ClueWriteExt: StateWrite { + fn put_current_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::current().to_string(), + count.to_be_bytes().to_vec(), + ) + } + + fn put_previous_clue_count(&mut self, count: u64) { + self.put_raw( + state_key::clue_count::previous().to_string(), + count.to_be_bytes().to_vec(), + ) + } +} + +impl ClueWriteExt for T {} + +#[async_trait] +trait ClueReadExt: StateRead { + async fn get_current_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::current()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } + + async fn get_previous_clue_count(&self) -> Result { + Ok(u64::from_be_bytes( + self.get_raw(state_key::clue_count::previous()) + .await? + .ok_or(anyhow!("no current clue count"))? + .as_slice() + .try_into()?, + )) + } +} + +impl ClueReadExt for T {} + +#[async_trait] +pub trait ClueManager: StateRead + StateWrite { + async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { + // Update count + { + let count = self.get_current_clue_count().await?; + self.put_current_clue_count(count.saturating_add(1)); + } + self.record_proto(pb::EventClue { + clue: Some(clue.into()), + tx: Some(tx.into()), + }); + Ok(()) + } +} + +impl ClueManager for T {} + +#[async_trait] +pub(crate) trait ClueManagerInternal: ClueManager { + fn init(&mut self) { + self.put_current_clue_count(0); + self.put_previous_clue_count(0); + } + + /// Flush the clue counts, returning the previous and current counts + async fn flush_clue_count(&mut self) -> Result<(u64, u64)> { + let previous = self.get_previous_clue_count().await?; + let current = self.get_current_clue_count().await?; + self.put_previous_clue_count(current); + self.put_current_clue_count(0); + Ok((previous, current)) + } +} + +impl ClueManagerInternal for T {} diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index c00e687a75..2e601eefa7 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use crate::fmd::{should_update_fmd_params, ClueManagerInternal as _}; +use super::fmd::ClueManagerInternal as _; +use crate::fmd::should_update_fmd_params; use crate::params::ShieldedPoolParameters; use crate::{fmd, genesis, state_key}; use anyhow::anyhow; diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index e500048aa6..a81d283ddb 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,12 +1,9 @@ use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use cnidarium::{StateRead, StateWrite}; -use decaf377_fmd::{Clue, Precision}; +use decaf377_fmd::Precision; use penumbra_proto::{ core::component::shielded_pool::v1::{self as pb}, - DomainType, StateWriteProto, + DomainType, }; -use penumbra_txhash::TransactionId; use serde::{Deserialize, Serialize}; pub mod state_key; @@ -107,7 +104,12 @@ impl Default for MetaParameters { } impl MetaParameters { - pub fn updated_fmd_params(&self, _old: &Parameters, height: u64, _clue_count_delta: (u64, u64)) -> Parameters { + pub fn updated_fmd_params( + &self, + _old: &Parameters, + height: u64, + _clue_count_delta: (u64, u64), + ) -> Parameters { match *self { MetaParameters::Fixed(precision) => Parameters { precision, @@ -116,84 +118,3 @@ impl MetaParameters { } } } - -#[async_trait] -trait ClueWriteExt: StateWrite { - fn put_current_clue_count(&mut self, count: u64) { - self.put_raw( - state_key::clue_count::current().to_string(), - count.to_be_bytes().to_vec(), - ) - } - - fn put_previous_clue_count(&mut self, count: u64) { - self.put_raw( - state_key::clue_count::previous().to_string(), - count.to_be_bytes().to_vec(), - ) - } -} - -impl ClueWriteExt for T {} - -#[async_trait] -trait ClueReadExt: StateRead { - async fn get_current_clue_count(&self) -> Result { - Ok(u64::from_be_bytes( - self.get_raw(state_key::clue_count::current()) - .await? - .ok_or(anyhow!("no current clue count"))? - .as_slice() - .try_into()?, - )) - } - - async fn get_previous_clue_count(&self) -> Result { - Ok(u64::from_be_bytes( - self.get_raw(state_key::clue_count::previous()) - .await? - .ok_or(anyhow!("no current clue count"))? - .as_slice() - .try_into()?, - )) - } -} - -impl ClueReadExt for T {} - -#[async_trait] -pub trait ClueManager: StateRead + StateWrite { - async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { - // Update count - { - let count = self.get_current_clue_count().await?; - self.put_current_clue_count(count.saturating_add(1)); - } - self.record_proto(pb::EventClue { - clue: Some(clue.into()), - tx: Some(tx.into()) - }); - Ok(()) - } -} - -impl ClueManager for T {} - -#[async_trait] -pub(crate) trait ClueManagerInternal: ClueManager { - fn init(&mut self) { - self.put_current_clue_count(0); - self.put_previous_clue_count(0); - } - - /// Flush the clue counts, returning the previous and current counts - async fn flush_clue_count(&mut self) -> Result<(u64, u64)> { - let previous = self.get_previous_clue_count().await?; - let current = self.get_current_clue_count().await?; - self.put_previous_clue_count(current); - self.put_current_clue_count(0); - Ok((previous, current)) - } -} - -impl ClueManagerInternal for T {} diff --git a/crates/core/component/shielded-pool/src/fmd/state_key.rs b/crates/core/component/shielded-pool/src/fmd/state_key.rs index 4a3d1fcc27..30bac089ec 100644 --- a/crates/core/component/shielded-pool/src/fmd/state_key.rs +++ b/crates/core/component/shielded-pool/src/fmd/state_key.rs @@ -10,7 +10,7 @@ pub mod parameters { } } -pub(super) mod clue_count { +pub(crate) mod clue_count { pub fn current() -> &'static str { "shielded_pool/fmd_clue_count/current" } diff --git a/crates/core/component/shielded-pool/src/lib.rs b/crates/core/component/shielded-pool/src/lib.rs index 96c97789df..84d112ed52 100644 --- a/crates/core/component/shielded-pool/src/lib.rs +++ b/crates/core/component/shielded-pool/src/lib.rs @@ -26,7 +26,6 @@ pub mod output; pub mod spend; pub use convert::{ConvertCircuit, ConvertProof, ConvertProofPrivate, ConvertProofPublic}; -pub use fmd::ClueManager; pub use nullifier_derivation::{ NullifierDerivationCircuit, NullifierDerivationProof, NullifierDerivationProofPrivate, NullifierDerivationProofPublic, From de7c7f1456881113de6b0c45ed5ff475843c4246 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Mon, 15 Apr 2024 15:16:38 -0700 Subject: [PATCH 07/14] Make the FMD parameter grace period a parameter --- .../action_handler/transaction/stateful.rs | 15 ++-- .../app_can_spend_notes_and_detect_outputs.rs | 2 +- .../src/component/shielded_pool.rs | 14 ++-- .../core/component/shielded-pool/src/fmd.rs | 65 +++++++++++------- ...enumbra.core.component.shielded_pool.v1.rs | 10 ++- ...a.core.component.shielded_pool.v1.serde.rs | 21 ++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 482213 -> 482521 bytes .../shielded_pool/v1/shielded_pool.proto | 6 +- 8 files changed, 93 insertions(+), 40 deletions(-) diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index 2a7c6f8285..de5dbda678 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -10,8 +10,6 @@ use penumbra_transaction::{Transaction, TransactionParameters}; use crate::app::StateReadExt; -const FMD_GRACE_PERIOD_BLOCKS: u64 = 10; - pub async fn tx_parameters_historical_check( state: S, transaction: &Transaction, @@ -73,6 +71,11 @@ pub async fn expiry_height_is_valid(state: S, expiry_height: u64) } pub async fn fmd_parameters_valid(state: S, transaction: &Transaction) -> Result<()> { + let meta_params = state + .get_shielded_pool_params() + .await + .expect("chain params request must succeed") + .fmd_meta_params; let previous_fmd_parameters = state .get_previous_fmd_parameters() .await @@ -84,6 +87,7 @@ pub async fn fmd_parameters_valid(state: S, transaction: &Transact let height = state.get_block_height().await?; fmd_precision_within_grace_period( transaction, + meta_params, previous_fmd_parameters, current_fmd_parameters, height, @@ -101,6 +105,7 @@ pub async fn fmd_parameters_valid(state: S, transaction: &Transact )] pub fn fmd_precision_within_grace_period( tx: &Transaction, + meta_params: fmd::MetaParameters, previous_fmd_parameters: fmd::Parameters, current_fmd_parameters: fmd::Parameters, block_height: u64, @@ -112,12 +117,12 @@ pub fn fmd_precision_within_grace_period( .fmd_clues { // Clue must be using the current `fmd::Parameters`, or be within - // `FMD_GRACE_PERIOD_BLOCKS` of the previous `fmd::Parameters`. + // `fmd_grace_period_blocks` of the previous `fmd::Parameters`. let clue_precision = clue.precision()?; let using_current_precision = clue_precision == current_fmd_parameters.precision; let using_previous_precision = clue_precision == previous_fmd_parameters.precision; - let within_grace_period = - block_height < previous_fmd_parameters.as_of_block_height + FMD_GRACE_PERIOD_BLOCKS; + let within_grace_period = block_height + < previous_fmd_parameters.as_of_block_height + meta_params.fmd_grace_period_blocks; if using_current_precision || (using_previous_precision && within_grace_period) { continue; } else { diff --git a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index bdf9bac193..661531ce32 100644 --- a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -2,11 +2,11 @@ use { self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + decaf377_fmd::Precision, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, }, - decaf377_fmd::Precision, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index 2e601eefa7..f6547926a5 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -73,13 +73,13 @@ impl Component for ShieldedPool { .height .try_into() .expect("height should not be negative"); - if should_update_fmd_params(height) { - let state = Arc::get_mut(state).expect("the state should not be shared"); - let meta_params = state - .get_shielded_pool_params() - .await - .expect("should be able to read state") - .fmd_meta_params; + let state = Arc::get_mut(state).expect("the state should not be shared"); + let meta_params = state + .get_shielded_pool_params() + .await + .expect("should be able to read state") + .fmd_meta_params; + if should_update_fmd_params(meta_params.fmd_grace_period_blocks, height) { let old = state .get_current_fmd_parameters() .await diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index a81d283ddb..f7d91432f9 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -9,14 +9,12 @@ use serde::{Deserialize, Serialize}; pub mod state_key; /// How long users have to switch to updated parameters. -pub const FMD_GRACE_PERIOD_BLOCKS: u64 = 1 << 4; -/// How often we update the params. -pub const FMD_UPDATE_FREQUENCY_BLOCKS: u64 = 1 << 6; -/// How many blocks we expect per day, approximately. -const _BLOCKS_PER_DAY: u64 = 1 << 13; - -pub fn should_update_fmd_params(height: u64) -> bool { - height % FMD_UPDATE_FREQUENCY_BLOCKS == 0 +pub const FMD_GRACE_PERIOD_BLOCKS_DEFAULT: u64 = 1 << 4; +/// How often we update the params, in terms of the number of grace periods +pub const FMD_UPDATE_FREQUENCY_GRACE_PERIOD: u64 = 4; + +pub fn should_update_fmd_params(fmd_grace_period_blocks: u64, height: u64) -> bool { + height % (fmd_grace_period_blocks * FMD_UPDATE_FREQUENCY_GRACE_PERIOD) == 0 } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -61,34 +59,50 @@ impl Default for Parameters { } } -/// Meta parameters are an algorithm for dynamically choosing FMD parameters. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(try_from = "pb::FmdMetaParameters", into = "pb::FmdMetaParameters")] -pub enum MetaParameters { +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MetaParametersAlgorithm { /// Used a fixed precision forever. Fixed(Precision), } +/// Meta paramaters governing how FMD parameters change. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "pb::FmdMetaParameters", into = "pb::FmdMetaParameters")] +pub struct MetaParameters { + pub fmd_grace_period_blocks: u64, + pub algorithm: MetaParametersAlgorithm, +} + impl TryFrom for MetaParameters { type Error = anyhow::Error; fn try_from(value: pb::FmdMetaParameters) -> Result { - match value.algorithm.ok_or(anyhow!("missing algorithm"))? { + let fmd_grace_period_blocks = value.fmd_grace_period_blocks; + let algorithm = match value + .algorithm + .ok_or(anyhow!("FmdMetaParameters missing algorithm"))? + { pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits(p) => { - Ok(MetaParameters::Fixed(Precision::new(p as u8)?)) + MetaParametersAlgorithm::Fixed(Precision::new(p as u8)?) } - } + }; + Ok(MetaParameters { + fmd_grace_period_blocks, + algorithm, + }) } } impl From for pb::FmdMetaParameters { fn from(value: MetaParameters) -> Self { - match value { - MetaParameters::Fixed(p) => pb::FmdMetaParameters { - algorithm: Some(pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits( - p.bits().into(), - )), - }, + let algorithm = match value.algorithm { + MetaParametersAlgorithm::Fixed(p) => { + pb::fmd_meta_parameters::Algorithm::FixedPrecisionBits(p.bits().into()) + } + }; + pb::FmdMetaParameters { + fmd_grace_period_blocks: value.fmd_grace_period_blocks, + algorithm: Some(algorithm), } } } @@ -99,7 +113,10 @@ impl DomainType for MetaParameters { impl Default for MetaParameters { fn default() -> Self { - Self::Fixed(Precision::default()) + Self { + fmd_grace_period_blocks: FMD_GRACE_PERIOD_BLOCKS_DEFAULT, + algorithm: MetaParametersAlgorithm::Fixed(Precision::default()), + } } } @@ -110,8 +127,8 @@ impl MetaParameters { height: u64, _clue_count_delta: (u64, u64), ) -> Parameters { - match *self { - MetaParameters::Fixed(precision) => Parameters { + match self.algorithm { + MetaParametersAlgorithm::Fixed(precision) => Parameters { precision, as_of_block_height: height, }, diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index dd2ece1b63..f378418ea2 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -60,18 +60,24 @@ impl ::prost::Name for GenesisContent { ) } } +/// The parameters which control how the FMD parameters evolve over time. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct FmdMetaParameters { - #[prost(oneof = "fmd_meta_parameters::Algorithm", tags = "1")] + /// How much time users have to transition to new parameters. + #[prost(uint64, tag = "1")] + pub fmd_grace_period_blocks: u64, + /// The algorithm governing how the parameters change. + #[prost(oneof = "fmd_meta_parameters::Algorithm", tags = "2")] pub algorithm: ::core::option::Option, } /// Nested message and enum types in `FmdMetaParameters`. pub mod fmd_meta_parameters { + /// The algorithm governing how the parameters change. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Algorithm { - #[prost(uint32, tag = "1")] + #[prost(uint32, tag = "2")] FixedPrecisionBits(u32), } } diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index 167ca4f8a6..bfd142412d 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -693,10 +693,17 @@ impl serde::Serialize for FmdMetaParameters { { use serde::ser::SerializeStruct; let mut len = 0; + if self.fmd_grace_period_blocks != 0 { + len += 1; + } if self.algorithm.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.FmdMetaParameters", len)?; + if self.fmd_grace_period_blocks != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("fmdGracePeriodBlocks", ToString::to_string(&self.fmd_grace_period_blocks).as_str())?; + } if let Some(v) = self.algorithm.as_ref() { match v { fmd_meta_parameters::Algorithm::FixedPrecisionBits(v) => { @@ -714,12 +721,15 @@ impl<'de> serde::Deserialize<'de> for FmdMetaParameters { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "fmd_grace_period_blocks", + "fmdGracePeriodBlocks", "fixed_precision_bits", "fixedPrecisionBits", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + FmdGracePeriodBlocks, FixedPrecisionBits, __SkipField__, } @@ -743,6 +753,7 @@ impl<'de> serde::Deserialize<'de> for FmdMetaParameters { E: serde::de::Error, { match value { + "fmdGracePeriodBlocks" | "fmd_grace_period_blocks" => Ok(GeneratedField::FmdGracePeriodBlocks), "fixedPrecisionBits" | "fixed_precision_bits" => Ok(GeneratedField::FixedPrecisionBits), _ => Ok(GeneratedField::__SkipField__), } @@ -763,9 +774,18 @@ impl<'de> serde::Deserialize<'de> for FmdMetaParameters { where V: serde::de::MapAccess<'de>, { + let mut fmd_grace_period_blocks__ = None; let mut algorithm__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::FmdGracePeriodBlocks => { + if fmd_grace_period_blocks__.is_some() { + return Err(serde::de::Error::duplicate_field("fmdGracePeriodBlocks")); + } + fmd_grace_period_blocks__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::FixedPrecisionBits => { if algorithm__.is_some() { return Err(serde::de::Error::duplicate_field("fixedPrecisionBits")); @@ -778,6 +798,7 @@ impl<'de> serde::Deserialize<'de> for FmdMetaParameters { } } Ok(FmdMetaParameters { + fmd_grace_period_blocks: fmd_grace_period_blocks__.unwrap_or_default(), algorithm: algorithm__, }) } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 731fe8752f868a4fe90b797163c47ad7dbda099b..491afabd9e133ca83425f95b88e3f7ad9e9c5abc 100644 GIT binary patch delta 4663 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 EKgNvVdjJ3c diff --git a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index a395781405..15bdee9a4a 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -29,9 +29,13 @@ message GenesisContent { repeated Allocation allocations = 3; } +// The parameters which control how the FMD parameters evolve over time. message FmdMetaParameters { + // How much time users have to transition to new parameters. + uint64 fmd_grace_period_blocks = 1; + // The algorithm governing how the parameters change. oneof algorithm { - uint32 fixed_precision_bits = 1; + uint32 fixed_precision_bits = 2; } } From b77baa5115ecf52fde1e850b736e700350305d6f Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 15 May 2024 15:09:53 -0700 Subject: [PATCH 08/14] Implement default value when reading clue counts --- .../shielded-pool/src/component/fmd.rs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/core/component/shielded-pool/src/component/fmd.rs b/crates/core/component/shielded-pool/src/component/fmd.rs index a9e42b15ba..a0c695e4c4 100644 --- a/crates/core/component/shielded-pool/src/component/fmd.rs +++ b/crates/core/component/shielded-pool/src/component/fmd.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use decaf377_fmd::Clue; @@ -31,24 +31,27 @@ impl ClueWriteExt for T {} #[async_trait] trait ClueReadExt: StateRead { + // The implementation for both of these methods will return 0 on a missing key, + // this is because the clue count is just used to tally clues over time, + // and so 0 will always be a good starting value. async fn get_current_clue_count(&self) -> Result { - Ok(u64::from_be_bytes( - self.get_raw(state_key::clue_count::current()) - .await? - .ok_or(anyhow!("no current clue count"))? - .as_slice() - .try_into()?, - )) + Ok(self + .get_raw(state_key::clue_count::current()) + .await? + .map(|x| x.as_slice().try_into()) + .transpose()? + .map(u64::from_be_bytes) + .unwrap_or(0u64)) } async fn get_previous_clue_count(&self) -> Result { - Ok(u64::from_be_bytes( - self.get_raw(state_key::clue_count::previous()) - .await? - .ok_or(anyhow!("no current clue count"))? - .as_slice() - .try_into()?, - )) + Ok(self + .get_raw(state_key::clue_count::previous()) + .await? + .map(|x| x.as_slice().try_into()) + .transpose()? + .map(u64::from_be_bytes) + .unwrap_or(0u64)) } } From f5e833ab7cd2ac7848ef0303f2d753a1bbe50a72 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 15 May 2024 16:08:50 -0700 Subject: [PATCH 09/14] Record clues in transaction action handler --- crates/core/app/src/action_handler/transaction.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index e454895f40..b8d2bb15a9 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -4,6 +4,7 @@ use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use penumbra_sct::{component::source::SourceContext, CommitmentSource}; +use penumbra_shielded_pool::component::ClueManager; use penumbra_transaction::Transaction; use tokio::task::JoinSet; use tracing::{instrument, Instrument}; @@ -105,6 +106,18 @@ impl AppActionHandler for Transaction { // Delete the note source, in case someone else tries to read it. state.put_current_source(None); + // Record all the clues in this transaction + // To avoid recomputing a hash. + let id = self.id(); + for clue in self + .transaction_body + .detection_data + .iter() + .flat_map(|x| x.fmd_clues.iter()) + { + state.record_clue(clue.clone(), id.clone()).await?; + } + Ok(()) } } From 3f933c1cc4d873a7c753c1afe1e0e865fa657044 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 15 May 2024 16:58:48 -0700 Subject: [PATCH 10/14] Implement migration for updating the ShieldedPoolParams and FMD Params We broke the protobuf for the shielded pool params, by changing the structure of the FMD params, and this migrates that to the default for the new format. --- "\\" | 641 ------------------ crates/bin/pd/src/migrate.rs | 1 + crates/bin/pd/src/migrate/testnet76.rs | 35 +- .../app_reproduce_testnet_75_vcb_close.rs | 5 +- 4 files changed, 32 insertions(+), 650 deletions(-) delete mode 100644 "\\" diff --git "a/\\" "b/\\" deleted file mode 100644 index 1bbf40a1b3..0000000000 --- "a/\\" +++ /dev/null @@ -1,641 +0,0 @@ -use std::{ - collections::BTreeMap, - fmt::{self, Debug, Formatter}, - mem, -}; - -use anyhow::{Context, Result}; -use penumbra_sct::epoch::Epoch; -use rand::{CryptoRng, RngCore}; -use rand_core::OsRng; -use tracing::instrument; - -use crate::{SpendableNoteRecord, ViewClient}; -use anyhow::anyhow; -use penumbra_asset::{asset, Value}; -use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan; -use penumbra_auction::auction::dutch::DutchAuctionDescription; -use penumbra_auction::auction::{ - dutch::actions::{ActionDutchAuctionEnd, ActionDutchAuctionSchedule}, - AuctionId, -}; -use penumbra_community_pool::CommunityPoolDeposit; -use penumbra_dex::{ - lp::action::{PositionClose, PositionOpen}, - lp::plan::PositionWithdrawPlan, - lp::position::{self, Position}, - lp::Reserves, - swap::SwapPlaintext, - swap::SwapPlan, - swap_claim::SwapClaimPlan, - TradingPair, -}; -use penumbra_fee::{Fee, FeeTier, GasPrices}; -use penumbra_governance::{ - proposal_state, DelegatorVotePlan, Proposal, ProposalDepositClaim, ProposalSubmit, - ProposalWithdraw, ValidatorVote, Vote, -}; -use penumbra_ibc::IbcRelay; -use penumbra_keys::{keys::AddressIndex, Address}; -use penumbra_num::Amount; -use penumbra_proto::view::v1::{NotesForVotingRequest, NotesRequest}; -use penumbra_shielded_pool::{Ics20Withdrawal, Note, OutputPlan, SpendPlan}; -use penumbra_stake::{rate::RateData, validator, IdentityKey, UndelegateClaimPlan}; -use penumbra_tct as tct; -use penumbra_transaction::{ - memo::MemoPlaintext, - plan::{ActionPlan, MemoPlan, TransactionPlan}, - ActionList, TransactionParameters, -}; - -/// A planner for a [`TransactionPlan`] that can fill in the required spends and change outputs upon -/// finalization to make a transaction balance. -pub struct Planner { - rng: R, - action_list: ActionList, - /// The fee tier to apply to this transaction. - fee_tier: FeeTier, - /// The set of prices used for gas estimation. - gas_prices: GasPrices, - /// The transaction parameters to use for the transaction. - transaction_parameters: TransactionParameters, - /// A user-specified change address, if any. - change_address: Option
, - /// A user-specified memo text, if any. - memo_text: Option, - /// A user-specified memo return address, if any. - memo_return_address: Option
, -} - -impl Debug for Planner { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("Planner") - .field("action_list", &self.action_list) - .field("fee_tier", &self.fee_tier) - .field("gas_prices", &self.gas_prices) - .field("transaction_parameters", &self.transaction_parameters) - .field("change_address", &self.change_address) - .field("memo_text", &self.memo_text) - .field("memo_return_address", &self.memo_return_address) - .finish() - } -} - -impl Planner { - /// Create a new planner. - pub fn new(rng: R) -> Self { - Self { - rng, - action_list: Default::default(), - gas_prices: Default::default(), - fee_tier: Default::default(), - transaction_parameters: Default::default(), - change_address: None, - memo_text: None, - memo_return_address: None, - } - } - - /// Add an arbitrary action to the planner. - pub fn action>(&mut self, action: A) -> &mut Self { - self.action_list.push(action); - self - } - - /// Set the current gas prices for fee prediction. - #[instrument(skip(self))] - pub fn set_gas_prices(&mut self, gas_prices: GasPrices) -> &mut Self { - self.gas_prices = gas_prices; - self - } - - /// Set the fee tier. - #[instrument(skip(self))] - pub fn set_fee_tier(&mut self, fee_tier: FeeTier) -> &mut Self { - self.fee_tier = fee_tier; - self - } - - /// Set the expiry height for the transaction. - #[instrument(skip(self))] - pub fn expiry_height(&mut self, expiry_height: u64) -> &mut Self { - self.transaction_parameters.expiry_height = expiry_height; - self - } - - /// Set a human-readable memo text for the transaction. - /// - /// Errors if the memo is too long. - #[instrument(skip(self))] - pub fn memo(&mut self, text: String) -> &mut Self { - self.memo_text = Some(text); - self - } - - /// Customize the return address for the memo. - /// - /// If unset, this will default to the address for the source account. This - /// must be an address controlled by the user, as the expectation is that - /// the recipient can use the address to transact with the sender. - #[instrument(skip(self))] - pub fn memo_return_address(&mut self, address: Address) -> &mut Self { - self.memo_return_address = Some(address); - self - } - - /// Set the change address for the transaction. - /// - /// If unset, this will default to the address for the source account. - /// - /// This can be a foreign address, allowing "send max" functionality. - #[instrument(skip(self))] - pub fn change_address(&mut self, address: Address) -> &mut Self { - self.change_address = Some(address); - self - } - - /// Spend a specific positioned note in the transaction. - #[instrument(skip(self))] - pub fn spend(&mut self, note: Note, position: tct::Position) -> &mut Self { - self.action_list - .push(SpendPlan::new(&mut self.rng, note, position)); - self - } - - /// Add an output note from this transaction. - /// - /// Any unused output value will be redirected back to the originating address as change notes. - #[instrument(skip(self))] - pub fn output(&mut self, value: Value, address: Address) -> &mut Self { - self.action_list - .push(OutputPlan::new(&mut self.rng, value, address)); - self - } - - /// Open a liquidity position in the order book. - #[instrument(skip(self))] - pub fn position_open(&mut self, position: Position) -> &mut Self { - self.action_list.push(PositionOpen { position }); - self - } - - /// Close a liquidity position in the order book. - #[instrument(skip(self))] - pub fn position_close(&mut self, position_id: position::Id) -> &mut Self { - self.action_list.push(PositionClose { position_id }); - self - } - - /// Withdraw a liquidity position in the order book. - /// - /// Note: Currently this only supports an initial withdrawal from Closed, with no rewards. - #[instrument(skip(self))] - pub fn position_withdraw( - &mut self, - position_id: position::Id, - reserves: Reserves, - pair: TradingPair, - ) -> &mut Self { - self.action_list.push(PositionWithdrawPlan { - reserves, - position_id, - pair, - sequence: 0, - rewards: Vec::new(), - }); - self - } - - /// Schedule a Dutch auction. - #[instrument(skip(self))] - pub fn dutch_auction_schedule(&mut self, description: DutchAuctionDescription) -> &mut Self { - self.action_list - .push(ActionDutchAuctionSchedule { description }); - self - } - - /// Ends a Dutch auction. - #[instrument(skip(self))] - pub fn dutch_auction_end(&mut self, auction_id: AuctionId) -> &mut Self { - self.action_list.push(ActionDutchAuctionEnd { auction_id }); - self - } - - /// Withdraws the reserves of the Dutch auction. - // TODO: nicer api? what do we get by passing fields individually rather than the plan? - #[instrument(skip(self))] - pub fn dutch_auction_withdraw(&mut self, plan: ActionDutchAuctionWithdrawPlan) -> &mut Self { - self.action_list.push(plan); - self - } - - /// Perform a swap based on input notes in the transaction. - #[instrument(skip(self))] - pub fn swap( - &mut self, - input_value: Value, - into_asset: asset::Id, - swap_claim_fee: Fee, - claim_address: Address, - ) -> Result<&mut Self> { - // Determine the canonical order for the assets being swapped. - // This will determine whether the input amount is assigned to delta_1 or delta_2. - let trading_pair = TradingPair::new(input_value.asset_id, into_asset); - - // If `trading_pair.asset_1` is the input asset, then `delta_1` is the input amount, - // and `delta_2` is 0. - // - // Otherwise, `delta_1` is 0, and `delta_2` is the input amount. - let (delta_1, delta_2) = if trading_pair.asset_1() == input_value.asset_id { - (input_value.amount, 0u64.into()) - } else { - (0u64.into(), input_value.amount) - }; - - // If there is no input, then there is no swap. - if delta_1 == Amount::zero() && delta_2 == Amount::zero() { - anyhow::bail!("No input value for swap"); - } - - // Create the `SwapPlaintext` representing the swap to be performed: - let swap_plaintext = SwapPlaintext::new( - &mut self.rng, - trading_pair, - delta_1, - delta_2, - swap_claim_fee, - claim_address, - ); - - let swap = SwapPlan::new(&mut self.rng, swap_plaintext); - self.action_list.push(swap); - - Ok(self) - } - - /// Perform a swap claim based on an input swap with a pre-paid fee. - #[instrument(skip(self))] - pub fn swap_claim(&mut self, plan: SwapClaimPlan) -> &mut Self { - self.action_list.push(plan); - self - } - - /// Add a delegation to this transaction. - #[instrument(skip(self))] - pub fn delegate( - &mut self, - epoch: Epoch, - unbonded_amount: Amount, - rate_data: RateData, - ) -> &mut Self { - let delegation = rate_data.build_delegate(epoch, unbonded_amount); - self.action_list.push(delegation); - self - } - - /// Add an undelegation to this transaction. - #[instrument(skip(self))] - pub fn undelegate( - &mut self, - epoch: Epoch, - delegation_amount: Amount, - rate_data: RateData, - ) -> &mut Self { - let undelegation = rate_data.build_undelegate(epoch, delegation_amount); - self.action_list.push(undelegation); - self - } - - /// Add an undelegate claim to this transaction. - #[instrument(skip(self))] - pub fn undelegate_claim(&mut self, claim_plan: UndelegateClaimPlan) -> &mut Self { - self.action_list.push(claim_plan); - self - } - - /// Upload a validator definition in this transaction. - #[instrument(skip(self))] - pub fn validator_definition(&mut self, new_validator: validator::Definition) -> &mut Self { - self.action_list.push(new_validator); - self - } - - /// Submit a new governance proposal in this transaction. - #[instrument(skip(self))] - pub fn proposal_submit(&mut self, proposal: Proposal, deposit_amount: Amount) -> &mut Self { - self.action_list.push(ProposalSubmit { - proposal, - deposit_amount, - }); - self - } - - /// Withdraw a governance proposal in this transaction. - #[instrument(skip(self))] - pub fn proposal_withdraw(&mut self, proposal: u64, reason: String) -> &mut Self { - self.action_list.push(ProposalWithdraw { proposal, reason }); - self - } - - /// Claim a governance proposal deposit in this transaction. - #[instrument(skip(self))] - pub fn proposal_deposit_claim( - &mut self, - proposal: u64, - deposit_amount: Amount, - outcome: proposal_state::Outcome<()>, - ) -> &mut Self { - self.action_list.push(ProposalDepositClaim { - proposal, - deposit_amount, - outcome, - }); - self - } - - /// Deposit a value into the Community Pool. - #[instrument(skip(self))] - pub fn community_pool_deposit(&mut self, value: Value) -> &mut Self { - self.action_list.push(CommunityPoolDeposit { value }); - self - } - - /// Cast a validator vote in this transaction. - #[instrument(skip(self))] - pub fn validator_vote(&mut self, vote: ValidatorVote) -> &mut Self { - self.action_list.push(vote); - self - } - - /// Perform an ICS-20 withdrawal - #[instrument(skip(self))] - pub fn ics20_withdrawal(&mut self, withdrawal: Ics20Withdrawal) -> &mut Self { - self.action_list.push(withdrawal); - self - } - - /// Perform an IBC action - #[instrument(skip(self))] - pub fn ibc_action(&mut self, ibc_action: IbcRelay) -> &mut Self { - self.action_list.push(ibc_action); - self - } - - /// Vote with all possible vote weight on a given proposal. - #[instrument(skip_all)] - pub async fn delegator_vote( - // TODO this sucks, why isn't there a bundle of proposal data to use for voting - // how is that not the thing returned by the rpc? why do we have to query a bunch of shit - // independently and stitch it together? - &mut self, - view: &mut V, - source: AddressIndex, - proposal: u64, - vote: Vote, - start_block_height: u64, - start_position: tct::Position, - start_rate_data: BTreeMap, - ) -> Result<&mut Self, anyhow::Error> { - let voting_notes = view - .notes_for_voting(NotesForVotingRequest { - votable_at_height: start_block_height, - address_index: Some(source.into()), - }) - .await?; - - anyhow::ensure!( - !voting_notes.is_empty(), - "no notes were found for voting on proposal {}", - proposal - ); - - // 1. Create a DelegatorVotePlan for each votable note. - for (record, ik) in &voting_notes { - let validator_start_rate_data = start_rate_data - .get(&ik) - .ok_or_else(|| anyhow!("missing rate data for votable note delegated to {}", ik))?; - - let voting_power_at_vote_start = - validator_start_rate_data.unbonded_amount(record.note.amount()); - - // 1. Create a DelegatorVotePlan that votes with this note on the proposal. - let plan = DelegatorVotePlan::new( - &mut self.rng, - proposal, - start_position, - vote, - record.note.clone(), - record.position, - voting_power_at_vote_start, - ); - self.delegator_vote_precise(plan); - } - - // 2. Here, we could sweep any spendable notes with delegation tokens to - // a new output to try to unlink them from a future vote. In practice - // this is meaningless because we don't have flow encryption, so - // delegator votes reveal the precise amount, and this amount will - // likely be unique to the delegator and enough to link their votes. - // Also, because we're in a single transaction, the pattern of - // delegations will also be revealed (vs creating distinct transactions - // for each validator). - // - // So instead, we do nothing. - - Ok(self) - } - - /// Vote with a specific positioned note in the transaction, rather than automatically. - #[instrument(skip(self, plan))] - pub fn delegator_vote_precise(&mut self, plan: DelegatorVotePlan) -> &mut Self { - self.action_list.push(plan); - self - } - - /// Prioritize notes to spend to release value of a specific transaction. - /// - /// Various logic is possible for note selection. Currently, this method - /// prioritizes notes sent to a one-time address, then notes with the largest - /// value: - /// - /// - Prioritizing notes sent to one-time addresses optimizes for a future in - /// which we implement DAGSync keyed by fuzzy message detection (which will not - /// be able to detect notes sent to one-time addresses). Spending these notes - /// immediately converts them into change notes, sent to the default address for - /// the users' account, which are detectable. - /// - /// - Prioritizing notes with the largest value optimizes for gas used by the - /// transaction. - /// - /// We may want to make note prioritization configurable in the future. For - /// instance, a user might prefer a note prioritization strategy that harvested - /// capital losses when possible, using cost basis information retained by the - /// view server. - pub fn prioritize_and_filter_spendable_notes( - &mut self, - records: Vec, - ) -> Vec { - let mut filtered = records - .into_iter() - .filter(|record| record.note.amount() > Amount::zero()) - .collect::>(); - filtered.sort_by(|a, b| { - // Sort by whether the note was sent to an ephemeral address... - match ( - a.address_index.is_ephemeral(), - b.address_index.is_ephemeral(), - ) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - // ... then by largest amount. - _ => b.note.amount().cmp(&a.note.amount()), - } - }); - filtered - } - - /// Add spends and change outputs as required to balance the transaction, using the view service - /// provided to supply the notes and other information. - pub async fn plan( - &mut self, - view: &mut V, - mut source: AddressIndex, - ) -> anyhow::Result { - // Wipe out the randomizer for the provided source, since - // 1. All randomizers correspond to the same account - // 2. Using one-time addresses for change addresses is undesirable. - source.randomizer = [0u8; 12]; - - // Compute the change address for this transaction. - let change_address = if let Some(ref address) = self.change_address { - address.clone() - } else { - view.address_by_index(source).await?.clone() - }; - - // Phase 1, "process all of the user-supplied intents into complete - // action plans", has already happened using the builder API. - // - // Compute an initial fee estimate based on the actions we have so far. - self.action_list.refresh_fee_and_change( - &mut self.rng, - &self.gas_prices, - &self.fee_tier, - &change_address, - ); - - // Phase 2: balance the transaction with information from the view service. - // - // It's possible that adding spends could increase the gas, increasing - // the fee amount, and so on, so we add spends iteratively. However, we - // need to query all the notes we'll use for planning upfront, so we - // don't accidentally try to use the same one twice. - - let mut notes_by_asset_id = BTreeMap::new(); - for required in self.action_list.balance_with_fee().required() { - // Find all the notes of this asset in the source account. - let records: Vec = view - .notes(NotesRequest { - include_spent: false, - asset_id: Some(required.asset_id.into()), - address_index: Some(source.into()), - amount_to_spend: None, - }) - .await?; - notes_by_asset_id.insert( - required.asset_id, - self.prioritize_and_filter_spendable_notes(records), - ); - } - - let mut iterations = 0usize; - - // Now iterate over the action list's imbalances to balance the transaction. - while let Some(required) = self.action_list.balance_with_fee().required().next() { - // Find a single note to spend towards the required balance. - let note = notes_by_asset_id - .get_mut(&required.asset_id) - .expect("we already made a notesrequest for each required asset") - .pop() - .ok_or_else(|| { - anyhow!( - "ran out of notes to spend while planning transaction, need {} of asset {}", - required.amount, - required.asset_id, - ) - })?; - - // Add a spend for that note to the action list. - self.action_list - .push(SpendPlan::new(&mut OsRng, note.note, note.position)); - - // Refresh the fee estimate and change outputs. - self.action_list.refresh_fee_and_change( - &mut self.rng, - &self.gas_prices, - &self.fee_tier, - &change_address, - ); - - iterations = iterations + 1; - if iterations > 100 { - return Err(anyhow!("failed to plan transaction after 100 iterations")); - } - } - - // Construct the memo plan for the transaction, using user-specified data if it - // was provided. - let memo_plan = if self.action_list.requires_memo() { - let return_address = if let Some(ref address) = self.memo_return_address { - // Check that this address is actually controlled by the user. - // We don't have an FVK, so we have to ask the view service. - anyhow::ensure!( - view.index_by_address(address.clone()).await?.is_some(), - "return address for memo is not controlled by the user", - ); - address.clone() - } else { - view.address_by_index(source).await?.clone() - }; - - Some(MemoPlan::new( - &mut self.rng, - MemoPlaintext::new(return_address, self.memo_text.take().unwrap_or_default()) - .context("could not create memo plaintext")?, - )) - } else { - None - }; - - // Configure the transaction parameters with the chain ID. - let app_params = view.app_params().await?; - let chain_id = app_params.chain_id.clone(); - self.transaction_parameters.chain_id = chain_id.clone(); - - // Fetch the FMD parameters that will be used to plan the transaction. - // (This really should have been considered witness data. Oh well.) - let fmd_params = view.fmd_parameters().await?; - - let plan = mem::take(&mut self.action_list).into_plan( - &mut self.rng, - &fmd_params, - self.transaction_parameters.clone(), - memo_plan, - )?; - - // Reset the planner in case it were reused. We don't want people to do that - // but otherwise we can't do builder method chaining with &mut self, and forcing - // the builder to move between calls is annoying for callers who are building up - // actions programmatically. Except we can't do a normal std::mem::replace here because - // the generic RNG mucks everything up. So it's just awful. - self.action_list = Default::default(); - self.gas_prices = Default::default(); - self.fee_tier = Default::default(); - self.transaction_parameters = Default::default(); - self.change_address = None; - self.memo_text = None; - self.memo_return_address = None; - - Ok(plan) - } -} diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index c38b57e328..99d2f61ca0 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -41,6 +41,7 @@ pub enum Migration { Testnet74, /// Testnet-76 migration: /// - Heal the auction component's VCB tally. + /// - Update FMD parameters to new protobuf structure. Testnet76, } diff --git a/crates/bin/pd/src/migrate/testnet76.rs b/crates/bin/pd/src/migrate/testnet76.rs index e407480dfb..b02b2b5598 100644 --- a/crates/bin/pd/src/migrate/testnet76.rs +++ b/crates/bin/pd/src/migrate/testnet76.rs @@ -6,10 +6,13 @@ use futures::TryStreamExt; use jmt::RootHash; use pbjson_types::Any; use penumbra_app::app::StateReadExt as _; +use penumbra_app::SUBSTORE_PREFIXES; use penumbra_asset::Balance; use penumbra_auction::auction::dutch::DutchAuction; use penumbra_proto::{DomainType, StateReadProto, StateWriteProto}; use penumbra_sct::component::clock::{EpochManager, EpochRead}; +use penumbra_shielded_pool::params::ShieldedPoolParameters; +use penumbra_shielded_pool::{component::StateWriteExt as _, fmd::Parameters as FmdParameters}; use std::path::PathBuf; use tracing::instrument; @@ -60,6 +63,17 @@ async fn heal_auction_vcb(delta: &mut StateDelta) -> anyhow::Result<() Ok(()) } +async fn write_shielded_pool_params(delta: &mut StateDelta) -> anyhow::Result<()> { + delta.put_shielded_pool_params(ShieldedPoolParameters::default()); + Ok(()) +} + +async fn write_fmd_params(delta: &mut StateDelta) -> anyhow::Result<()> { + delta.put_previous_fmd_parameters(FmdParameters::default()); + delta.put_current_fmd_parameters(FmdParameters::default()); + Ok(()) +} + /// Run the full migration, given an export path and a start time for genesis. /// /// Menu: @@ -71,11 +85,10 @@ pub async fn migrate( genesis_start: Option, ) -> anyhow::Result<()> { // Setup: - let snapshot = storage.latest_snapshot(); - let chain_id = snapshot.get_chain_id().await?; - let root_hash = snapshot.root_hash().await.expect("can get root hash"); + let export_state = storage.latest_snapshot(); + let root_hash = export_state.root_hash().await.expect("can get root hash"); let pre_upgrade_root_hash: RootHash = root_hash.into(); - let pre_upgrade_height = snapshot + let pre_upgrade_height = export_state .get_block_height() .await .expect("can get block height"); @@ -83,10 +96,14 @@ pub async fn migrate( // We initialize a `StateDelta` and start by reaching into the JMT for all entries matching the // swap execution prefix. Then, we write each entry to the nv-storage. - let mut delta = StateDelta::new(snapshot); + let mut delta = StateDelta::new(export_state); let (migration_duration, post_upgrade_root_hash) = { let start_time = std::time::SystemTime::now(); + // Set shield pool params to the new default + write_shielded_pool_params(&mut delta).await?; + // Initialize fmd params + write_fmd_params(&mut delta).await?; // Reconstruct a VCB balance for the auction component. heal_auction_vcb(&mut delta).await?; @@ -95,16 +112,20 @@ pub async fn migrate( tracing::info!(?post_upgrade_root_hash, "post-upgrade root hash"); ( - start_time.elapsed().expect("start time is set"), + start_time.elapsed().expect("start time not set"), post_upgrade_root_hash, ) }; storage.release().await; + let rocksdb_dir = pd_home.join("rocksdb"); + let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; + let migrated_state = storage.latest_snapshot(); // The migration is complete, now we need to generate a genesis file. To do this, we need // to lookup a validator view from the chain, and specify the post-upgrade app hash and // initial height. + let chain_id = migrated_state.get_chain_id().await?; let app_state = penumbra_app::genesis::Content { chain_id, ..Default::default() @@ -121,7 +142,6 @@ pub async fn migrate( tracing::info!(%now, "no genesis time provided, detecting a testing setup"); now }); - let checkpoint = post_upgrade_root_hash.0.to_vec(); let genesis = TestnetConfig::make_checkpoint(genesis, Some(checkpoint)); @@ -131,6 +151,7 @@ pub async fn migrate( std::fs::write(genesis_path, genesis_json).expect("can write genesis"); let validator_state_path = pd_home.join("priv_validator_state.json"); + let fresh_validator_state = crate::testnet::generate::TestnetValidator::initial_state(); std::fs::write(validator_state_path, fresh_validator_state).expect("can write validator state"); diff --git a/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs b/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs index c81b7eeba7..e88c6afd26 100644 --- a/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs +++ b/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs @@ -1,3 +1,4 @@ +use decaf377_fmd::Precision; use penumbra_auction::StateReadExt as _; use tracing_subscriber::filter::EnvFilter; use { @@ -161,7 +162,7 @@ async fn app_can_reproduce_tesnet_75_vcb_close() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(&mut OsRng, 0); + plan.populate_detection_data(&mut OsRng, Precision::default()); let tx = client.witness_auth_build(&mut plan).await?; node.block() @@ -232,7 +233,7 @@ async fn app_can_reproduce_tesnet_75_vcb_close() -> anyhow::Result<()> { ..Default::default() }, }; - plan.populate_detection_data(&mut OsRng, 0); + plan.populate_detection_data(&mut OsRng, Precision::default()); let tx = client.witness_auth_build(&mut plan).await?; tracing::info!("closing the auction"); From 8376a4196835e52828fae703c728c8d7c70f8800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Tue, 21 May 2024 11:30:40 -0700 Subject: [PATCH 11/14] Update crates/core/component/shielded-pool/src/component/shielded_pool.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Erwan Or Signed-off-by: Lúcás Meier --- .../core/component/shielded-pool/src/component/shielded_pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/shielded-pool/src/component/shielded_pool.rs b/crates/core/component/shielded-pool/src/component/shielded_pool.rs index f6547926a5..ec05edbacc 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -64,7 +64,7 @@ impl Component for ShieldedPool { ) { } - #[instrument(name = "shielded_pool", skip(state, end_block))] + #[instrument(name = "shielded_pool", skip_all)] async fn end_block( state: &mut Arc, end_block: &abci::request::EndBlock, From 0f8b499077b8d22889364852a440eccb9569d59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Tue, 21 May 2024 11:31:00 -0700 Subject: [PATCH 12/14] Update crates/core/component/shielded-pool/src/fmd.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Erwan Or Signed-off-by: Lúcás Meier --- crates/core/component/shielded-pool/src/fmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index f7d91432f9..5b9f4a941b 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -65,7 +65,7 @@ pub enum MetaParametersAlgorithm { Fixed(Precision), } -/// Meta paramaters governing how FMD parameters change. +/// Meta parameters governing how FMD parameters change. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "pb::FmdMetaParameters", into = "pb::FmdMetaParameters")] pub struct MetaParameters { From b066c1f352b3b1aa77124fe9f4f7da5399486631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Tue, 21 May 2024 11:31:40 -0700 Subject: [PATCH 13/14] Update crates/core/component/shielded-pool/src/fmd.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Erwan Or Signed-off-by: Lúcás Meier --- crates/core/component/shielded-pool/src/fmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/shielded-pool/src/fmd.rs b/crates/core/component/shielded-pool/src/fmd.rs index 5b9f4a941b..81cff0f392 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -61,7 +61,7 @@ impl Default for Parameters { #[derive(Clone, Debug, PartialEq, Eq)] pub enum MetaParametersAlgorithm { - /// Used a fixed precision forever. + /// Use a fixed precision forever. Fixed(Precision), } From 11db5232bcc644c6e2c419e4bc7580664e277acd Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 21 May 2024 11:33:51 -0700 Subject: [PATCH 14/14] Suggested review changes --- .../shielded-pool/src/component/fmd.rs | 3 +-- ...enumbra.core.component.shielded_pool.v1.rs | 6 +++--- ...a.core.component.shielded_pool.v1.serde.rs | 16 ++++++++-------- .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 482521 -> 482531 bytes .../shielded_pool/v1/shielded_pool.proto | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/core/component/shielded-pool/src/component/fmd.rs b/crates/core/component/shielded-pool/src/component/fmd.rs index a0c695e4c4..a827559307 100644 --- a/crates/core/component/shielded-pool/src/component/fmd.rs +++ b/crates/core/component/shielded-pool/src/component/fmd.rs @@ -60,12 +60,11 @@ impl ClueReadExt for T {} #[async_trait] pub trait ClueManager: StateRead + StateWrite { async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { - // Update count { let count = self.get_current_clue_count().await?; self.put_current_clue_count(count.saturating_add(1)); } - self.record_proto(pb::EventClue { + self.record_proto(pb::EventBroadcastClue { clue: Some(clue.into()), tx: Some(tx.into()), }); diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index f378418ea2..c443ef6760 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -300,7 +300,7 @@ impl ::prost::Name for EventOutput { /// ABCI Event recording a clue. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct EventClue { +pub struct EventBroadcastClue { #[prost(message, optional, tag = "1")] pub clue: ::core::option::Option< super::super::super::super::crypto::decaf377_fmd::v1::Clue, @@ -308,8 +308,8 @@ pub struct EventClue { #[prost(message, optional, tag = "2")] pub tx: ::core::option::Option, } -impl ::prost::Name for EventClue { - const NAME: &'static str = "EventClue"; +impl ::prost::Name for EventBroadcastClue { + const NAME: &'static str = "EventBroadcastClue"; const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; fn full_name() -> ::prost::alloc::string::String { ::prost::alloc::format!( diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index bfd142412d..bd21b555ee 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -382,7 +382,7 @@ impl<'de> serde::Deserialize<'de> for AssetMetadataByIdsResponse { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.AssetMetadataByIdsResponse", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for EventClue { +impl serde::Serialize for EventBroadcastClue { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -396,7 +396,7 @@ impl serde::Serialize for EventClue { if self.tx.is_some() { len += 1; } - let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", len)?; + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventBroadcastClue", len)?; if let Some(v) = self.clue.as_ref() { struct_ser.serialize_field("clue", v)?; } @@ -406,7 +406,7 @@ impl serde::Serialize for EventClue { struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for EventClue { +impl<'de> serde::Deserialize<'de> for EventBroadcastClue { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where @@ -454,13 +454,13 @@ impl<'de> serde::Deserialize<'de> for EventClue { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = EventClue; + type Value = EventBroadcastClue; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventClue") + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventBroadcastClue") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { @@ -485,13 +485,13 @@ impl<'de> serde::Deserialize<'de> for EventClue { } } } - Ok(EventClue { + Ok(EventBroadcastClue { clue: clue__, tx: tx__, }) } } - deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventClue", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventBroadcastClue", FIELDS, GeneratedVisitor) } } impl serde::Serialize for EventOutput { diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 491afabd9e133ca83425f95b88e3f7ad9e9c5abc..fc5ede0650b95024f9d736abd30774a6bbc79f48 100644 GIT binary patch delta 80 zcmcb)Q}*#r*$uyHm@ef_Tf`_eecc>ZmCaVQx}1XTj9fylWvO{3PDS~NDanb&oAcU) fBpIce8|Sw-&SwN-CLm@8Viq7~-QGB#En+VKKHVUA delta 72 zcmaF7Q}*Ug*$uyHSWe_|*-Uo~WtN<*zg2m&Rjn>3dmR_2YguaE=885UNk+lu?)mNA X^BIAd35c12m<5Pgw|CEHi`WYQA|@Qu diff --git a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index 15bdee9a4a..72ed58300b 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -111,7 +111,7 @@ message EventOutput { } // ABCI Event recording a clue. -message EventClue { +message EventBroadcastClue { crypto.decaf377_fmd.v1.Clue clue = 1; txhash.v1.TransactionId tx = 2; }