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/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/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index cb5f8b9377..6e23fc5c3f 100644 Binary files a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs and b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs differ 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..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(()) } } @@ -172,7 +185,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..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, @@ -93,14 +97,15 @@ 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, ) )] 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`. - 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 within_grace_period = - block_height < previous_fmd_parameters.as_of_block_height + FMD_GRACE_PERIOD_BLOCKS; + // `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 + meta_params.fmd_grace_period_blocks; if using_current_precision || (using_previous_precision && within_grace_period) { continue; } else { 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/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..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,6 +2,7 @@ use { self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + decaf377_fmd::Precision, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -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_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"); 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/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..a827559307 --- /dev/null +++ b/crates/core/component/shielded-pool/src/component/fmd.rs @@ -0,0 +1,94 @@ +use 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 { + // 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(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(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)) + } +} + +impl ClueReadExt for T {} + +#[async_trait] +pub trait ClueManager: StateRead + StateWrite { + async fn record_clue(&mut self, clue: Clue, tx: TransactionId) -> Result<()> { + { + let count = self.get_current_clue_count().await?; + self.put_current_clue_count(count.saturating_add(1)); + } + self.record_proto(pb::EventBroadcastClue { + 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 8e7ebaab6e..ec05edbacc 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,7 @@ use std::sync::Arc; +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; @@ -62,11 +64,34 @@ 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, + state: &mut Arc, + end_block: &abci::request::EndBlock, ) { + let height: u64 = end_block + .height + .try_into() + .expect("height should not be negative"); + 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 + .expect("should be able to read state"); + 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); + } } 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 58ae8bce2f..81cff0f392 100644 --- a/crates/core/component/shielded-pool/src/fmd.rs +++ b/crates/core/component/shielded-pool/src/fmd.rs @@ -1,13 +1,27 @@ -use penumbra_proto::{core::component::shielded_pool::v1 as pb, DomainType}; +use anyhow::{anyhow, Result}; +use decaf377_fmd::Precision; +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_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)] #[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, } @@ -19,9 +33,9 @@ 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_bits: msg.precision_bits.try_into()?, + precision: msg.precision_bits.try_into()?, as_of_block_height: msg.as_of_block_height, }) } @@ -30,7 +44,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,8 +53,85 @@ impl From for pb::FmdParameters { impl Default for Parameters { fn default() -> Self { Self { - precision_bits: 0, + precision: Precision::default(), as_of_block_height: 1, } } } + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MetaParametersAlgorithm { + /// Use a fixed precision forever. + Fixed(Precision), +} + +/// 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 { + pub fmd_grace_period_blocks: u64, + pub algorithm: MetaParametersAlgorithm, +} + +impl TryFrom for MetaParameters { + type Error = anyhow::Error; + + fn try_from(value: pb::FmdMetaParameters) -> Result { + 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) => { + MetaParametersAlgorithm::Fixed(Precision::new(p as u8)?) + } + }; + Ok(MetaParameters { + fmd_grace_period_blocks, + algorithm, + }) + } +} + +impl From for pb::FmdMetaParameters { + fn from(value: MetaParameters) -> Self { + 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), + } + } +} + +impl DomainType for MetaParameters { + type Proto = pb::FmdMetaParameters; +} + +impl Default for MetaParameters { + fn default() -> Self { + Self { + fmd_grace_period_blocks: FMD_GRACE_PERIOD_BLOCKS_DEFAULT, + algorithm: MetaParametersAlgorithm::Fixed(Precision::default()), + } + } +} + +impl MetaParameters { + pub fn updated_fmd_params( + &self, + _old: &Parameters, + height: u64, + _clue_count_delta: (u64, u64), + ) -> Parameters { + match self.algorithm { + MetaParametersAlgorithm::Fixed(precision) => Parameters { + precision, + as_of_block_height: height, + }, + } + } +} 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..30bac089ec 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(crate) 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/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/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/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 652751daeb..d54c58b2cb 100644 --- a/crates/crypto/decaf377-fmd/src/lib.rs +++ b/crates/crypto/decaf377-fmd/src/lib.rs @@ -11,11 +11,12 @@ 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; -/// The maximum detection precision, chosen so that the message bits fit in 3 bytes. -pub const MAX_PRECISION: usize = 24; +pub(crate) use precision::MAX_PRECISION; 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/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index b2c3ff37c5..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 @@ -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,36 @@ 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 { + /// 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 = "2")] + 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)] @@ -264,6 +297,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 EventBroadcastClue { + #[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 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!( + "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 67ce5950df..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,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 EventBroadcastClue { + #[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.EventBroadcastClue", 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 EventBroadcastClue { + #[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 = EventBroadcastClue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventBroadcastClue") + } + + 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(EventBroadcastClue { + clue: clue__, + tx: tx__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventBroadcastClue", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for EventOutput { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -573,6 +685,127 @@ 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.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) => { + 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] = &[ + "fmd_grace_period_blocks", + "fmdGracePeriodBlocks", + "fixed_precision_bits", + "fixedPrecisionBits", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + FmdGracePeriodBlocks, + 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 { + "fmdGracePeriodBlocks" | "fmd_grace_period_blocks" => Ok(GeneratedField::FmdGracePeriodBlocks), + "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 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")); + } + 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 { + fmd_grace_period_blocks: fmd_grace_period_blocks__.unwrap_or_default(), + 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 +2477,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 +2499,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 +2530,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 +2551,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 +2560,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 +2573,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 6c72f839f9..fc5ede0650 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ 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")) } } 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 979b0c35f7..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 @@ -5,12 +5,15 @@ import "penumbra/core/asset/v1/asset.proto"; import "penumbra/core/component/sct/v1/sct.proto"; import "penumbra/core/keys/v1/keys.proto"; import "penumbra/core/num/v1/num.proto"; +import "penumbra/core/txhash/v1/txhash.proto"; +import "penumbra/crypto/decaf377_fmd/v1/decaf377_fmd.proto"; import "penumbra/crypto/decaf377_rdsa/v1/decaf377_rdsa.proto"; import "penumbra/crypto/tct/v1/tct.proto"; // Configuration data for the shielded pool component. message ShieldedPoolParameters { - FmdParameters fixed_fmd_params = 1; + FmdParameters fixed_fmd_params = 1 [deprecated = true]; + FmdMetaParameters fmd_meta_params = 2; } // Genesis data for the shielded pool component. @@ -26,6 +29,16 @@ 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 = 2; + } +} + // Parameters for Fuzzy Message Detection message FmdParameters { uint32 precision_bits = 1; @@ -97,6 +110,12 @@ message EventOutput { crypto.tct.v1.StateCommitment note_commitment = 1; } +// ABCI Event recording a clue. +message EventBroadcastClue { + crypto.decaf377_fmd.v1.Clue clue = 1; + txhash.v1.TransactionId tx = 2; +} + // 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.