From b5d13e9dded1fbdce63640682898f1f079d689a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Tue, 21 May 2024 12:13:05 -0700 Subject: [PATCH] FMD Parameter Selection Breaking Changes (#4137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first part of #1087. This restructures the way FMD parameters are handled so that we can choose different algorithms to update the parameters. A migration has been written so that we move from the old format, where we had a hard-coded parameter, to a new format, where we have an algorithm as a chain parameter, and then update the current FMD parameter over time based on that algorithm. The protobufs are also structured so that we can easily add new algorithms over time, without making a breaking state change. By default, the algorithm just chooses a constant parameter, so dynamic FMD params are not part of this PR. --------- Signed-off-by: Lúcás Meier Co-authored-by: Erwan Or --- Cargo.lock | 1 + crates/bin/pd/src/migrate.rs | 1 + crates/bin/pd/src/migrate/testnet76.rs | 35 ++- .../src/gen/proto_descriptor.bin.no_lfs | Bin 99467 -> 99489 bytes crates/core/app/Cargo.toml | 3 +- .../app/src/action_handler/transaction.rs | 15 +- .../action_handler/transaction/stateful.rs | 25 +- crates/core/app/src/params/change.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 +- .../app_reproduce_testnet_75_vcb_close.rs | 5 +- ..._uptime_for_validators_only_once_active.rs | 7 +- .../component/shielded-pool/src/component.rs | 2 + .../shielded-pool/src/component/fmd.rs | 94 +++++++ .../src/component/shielded_pool.rs | 31 ++- .../core/component/shielded-pool/src/fmd.rs | 105 +++++++- .../shielded-pool/src/fmd/state_key.rs | 10 + .../component/shielded-pool/src/params.rs | 12 +- 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/hash.rs | 2 +- crates/crypto/decaf377-fmd/src/lib.rs | 5 +- crates/crypto/decaf377-fmd/src/precision.rs | 79 ++++++ crates/crypto/decaf377-fmd/tests/fmd.rs | 17 +- ...enumbra.core.component.shielded_pool.v1.rs | 53 ++++ ...a.core.component.shielded_pool.v1.serde.rs | 251 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 481372 -> 482531 bytes crates/proto/src/protobuf.rs | 10 +- .../shielded_pool/v1/shielded_pool.proto | 21 +- 40 files changed, 799 insertions(+), 120 deletions(-) create mode 100644 crates/core/component/shielded-pool/src/component/fmd.rs 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/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 cb5f8b9377fc15a61da07be47f5ef6ad339b0300..6e23fc5c3fa90b2ae6af49ab1a84a133ac4ac9bb 100644 GIT binary patch delta 20407 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 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 6c72f839f9e5ed8c07f1477d7b062a002404e36a..fc5ede0650b95024f9d736abd30774a6bbc79f48 100644 GIT binary patch delta 26205 zcmZvl37AyH)%U0B-tODovoO;$Yy)UBvM9UY0xBvZ8cl+0j1jjH2AKqA3^Qnq#ulTf zC>m_xiu$U&5Qz(JygH&mF^U8+8kNL#1SO)PAdA5eiTQq~s&3!T`+h!;kMpmp^RH8< zs!mm%s=j{kSf9-+`#iSvvK3c6e4_5y`Ks4jtyp^5Zson%k_mr4(d!wuoa6T^ynXqz zh16pM!YP0DZq|QOg)e^DQ#BP5SDZHRINd{?UNyD(7fn-6m@(zl#+i*X+Om@~U8c^M za>0zI_Qne?F-hUhR@L{mn``PR^h1(O!KU`6nQf<6QdE*wTzJJ?mA^@4YW`1|?4gq_=T+%eNxpm3~7fo-S{Ij+mDyI(gPp>2UM9DqXWFBYovLiEfMzaepnb|bC zxvjai<${Zv+uM3*WGcT`c6!$2o*HEy*W6y!c3gQT-8lW?)|t)i(`Kyr{K|Fyfa_Hz zJK@r%miFUjwl+?g+}PGWe)_DY?3j!{ng1FM%18DOAHT%!6@L4cSKh^>XAjHh_FqU{ zq58X57hb*5>+X@V;_=xRm1lO1&ji^-S=l$Lu(+izJa)2QUN~yrkfFme>5TRgSua&~ zl=(B6_0nZWn?HlBmnj>R8JE$1S=O&Ct1jf{jmuZ2_RExKe7{VWWxXmrCR33~2tcq( zcZrY!Rp%n)fez>wp%ke9=uBlMDRL>v9B{bfM6QcI(3MnhcG0yFlDxV;2SgCLE&~Q; zjzm3`^=r%esQTgIewV6lh5c@6$k(R28A>H;*6EQ(3IJuD?hzp<>v|525R7#r4hH$Y z@@R5b-P5yqUKu63X6lU6u3l%o9{#Xmwx(4*{Jt?(IT9KUiW89UF=S}wU}>6$ym#UJ zTSw)4>x^r=n7Vg)*9b|8Zha$!aPNNoGiP9Fo%M5N16BQrxihCV<;-L=(<*n_wC2gv za+6zI+Gn;-&rNH+EZ0tf6MlGnX`!Y|Tc=;zlxw}TX=bjydBzW#hGqE2^F^avs%PeV zsHerY4Z45*k-6`|GGi9Blx@n*YGeB5rZqy)-kNKl+1S$7+|C>m)Y5cWc-pCcUA_!M zHt3Ym942@h(smevXh#FX@HnX@MgR8G1HAe}OiPMWFn6(N*V5c_aj_94t(-iqvE^dX zS_VBF@b^pAn1QCH5u^M2c9?_Eq(5D1N@_)B|MLAKgeLtP4#*sWMZK&)sBCCq-K`_? z9)$*_x*9r9%rIE@HWC2X2D^5FC^$IXJ<5Y^aD6W)M%EB*o9r1Sh7`p_i6QA;PD~*- zWWW9C0PXu(e|TAblrp`+bn=F$Dh!`5YK+iU2EZ^Gp{or8h=L<(xmOP+ z#%iCS)S;VqWD zsSiSp!;SjBpAhXw>jp{n@6g=9g0fO&n)c7TQLDnB%_I*{~Qh4Creuee7ostbGJ3dv5 zEBF0CNGBAA6vmF=PgI=X>;t0MgxqkWA_&R}BS$$QvQ8{4DvUiiFy=VX=>d`)2Ca;9 z5JD&R=$ZMoGF4%ZlMBJ^d8!g?oa}}U2&t26Y{!6*I+^)!5Zb4*{=~9V3s2qN|D+U! zCc62M617ipwg6y@Q=ASU3ZCL@0fOxmY;hoL>8yW7*^dhS7W6wQO`$VVb!K3sh3!no z27v8M#|EO{nT`zv+nKN(4qGPcpIvrNVg76RTaI%Z+tG6uY3pPCpP_ z7qBQCZiL8cEZntl6hh+68}$$;BpuW^^w0<))Hr%f=KDs7{+V3(ZsCatN&igNRj!!y z&*Uz4s*s1!t9^fT$mLOJgtI47pJnu1{I?9G(Ez|17Mq`?Fs^-;IxLp zQ66m5h7HdQg{?B{PcLgx^?h^WjvIec;pfxp@~yMlFPYWuR)$I>r!#V8$5AO&%+QAz z8UR(yaAOxl!5MDsf~aB!V|Nf$RAv32m$j?<{Y(`(v$M*v2Wy=gZ6`2@`eyEDr#guGX7(Ro)T|QQwYjzf5Np%cYk?%M-gG{>^Q+mE%~rIb3c_l(Q^{+T zzD{{;ff9nrzJ|>=*-3#k(*L#c>oQ%~QW=a}Bq5)UQxL(gbcqvKJAbXRwM7mJT&w(U z#R9Zf5-Q>ZQ=+maPJrTCRVOzgUcgK;kl}! zHiA&kRo!GGAhKlMB+WM#md+NKiJ-|XuZbX{H%Y^#=W4RaTUFWJuIFkfd8_o?A+QED zqSAa-=z90y!-W_O@qDF|h6zaV`6{p~0wJESs=8#3K)ObBDiqr9K5P_!vP>5gyAeyn z@B^l;LgXS>5QU`H8dg$&L80^R5y)xe7APF5l@rPZVlW4hTcEmj%N%dyKw;t7|2lSb zjT~$IFqWfpfh?_7&Xbag{fcZlYcxBcMN0RnKO$7OctZ>4-IJwe9|lsRn&q6aNTqD& zkPE{i)vI^rB*Q@2y9$HunQ%yrPRQS}99t4#Q7(bpT_QIG(`U2EdsW%e!jgLi)@CVp zuS!*AMnaesyDu(u-ZN|ze`4~*N>>=&ffQV<7{<~RAcTv>bcY%_GM5yNzW3lmvt|q| zQ5?%`K{5FfRoyj$$SqN<)_)NHQ=3gbpvqPhmfpLnww8hqh>cH&u~syBsL+1j#8Lc- zCJ!lHX()gcd`MN*M-aw`6uYPsjU1WF3xB_FJaRG`mn%G$XabZhh0Agfx#bF<#UYnm zI{(qa&lVqg2!CSAM^6ZGk)@ zEa;MAy`gZ3?N+Mp4Mh$y*Gk2*Wm}5#`@Pc76a31@9}wkNN4~F4 z+V+CzCgZ72|3C}~w$6`SlkJ=z5ZM<*E9w2N+2k5k_NMFou9RFOy)Q@QuAlJ%MOCx=^Ua^xDx7StBpN!k%Pvrg-%gMn1Q{?u@ zm6D>TIIXVY_FfmaccgWDo%h#Lv$#D{t=j{_@Om`ofH1r+bM9orK-o7658i*m=&r`? zy%Eca+j}FH6Swz<$PrLVG7$cv%05)8epEQ(aLzu*Eo=DxUliMOMncj|s=q3ImNZrn zOn+5LGkFDKrN62kBcfc0|Ei8WGxL@$islJ)(#AkIE&aH*Y2WKC`pKxwfgr|fA zNXf0L=fDWUxm67soH+p{dt{T_RoSk>A08Z6*MoA~h4Eft>>-Rh3*8^O>pcFXgLf*z zJi-X1f}N_|Y6(Per|Nr9luHFW6=5Qo7Cp1cuNCn^yPNAt$*&dXonxq>r_`{g(BScKRaH(Po0}oYdeN4Q-M;`!b5(i&gm%pPK`{d zS2|vPOdX*QIy_T%$+Sx@xVWi>*y`j9i27XGJh`c@#~`ApsUs3o8>hE5ot~vg$Brjd zlkS-Gq#9qP_oX}Tdy&v=*Hp%zc*?{HY(F!ZE36Tycd!ggx*VK(^bwycPdrW}u?69R%4HdMg<7Nv1V-i1YJG5>`%Ho!%Bq%S<6zwx?Cl$;#qH>%P3 z$wlb|XP(LBHq1i_q$l^XM>-JFlbKL*x@1F}^`_`37@S0tDMgZ`Oyx=12O*uuDzC_TmxbBQYWV2OiqaKSep$JVEm8vM%W9+ktw8#+ zKKq;4kIp8pD7!2Cfa@BzcITkr=yWZAXra->PB7-s? zvB)04%e&sDBqVv`t))FU+Sy7EHm)3m-v?U1uXWzTOV3tJ16c>i#!Jsuy#_?k2y;Cl z+c439qd7CdmX+WzFrt!h5=Kpc^W7y$#u%_(@>E+sf9pHTl1w7i;?Z4$)#?x?LOEfu-&*$Rc7N75MHNhd&CK7 zaGh*3q8cbLC%pd?wO@WtDU+N==ae$Z#%zwNwfkkMOLmEKWUC}+e@S2qaf9-Q7poFO zkq~PqTkacFrQKm02{><12MjVId6NR^4T|YuO^p(B6@725$6-!jWNKU&kZI;}A)Bk} z?P|^T5T?0ywU$n!?9EEs0~H7+V8n#~@^8*729l-1gOQyZ6hWwNj+G*Se2dc2;J}9o zjHqy8z@k!kF<^aP$AlXLa#{4S%zhbXNsjyrl~;e43den_?yo7ROn28evdsCnhyVCg z?RWg`u^maqEVsvfV-`CSioI;sI}(b$EK8ohAUx+YdU-)9ll1a}QYPu;1#vH%wT}dP z*>(uwXJQ6JTyt=}0;9I$dx7P4f2PIxN(jO3&w#W&R8_1eCi<;4!RI@}#OLa|AKw{s zq|xY3IV0Jg!1V&NQODqWfemtrBx_spzf{?u-4;yd?0>0LpNu$nxgPTG4L|x^op#>6 z5jlw{cyBa0%{>ut|52tcAmsO|!^W7&>B;0Irtq{ojDe|OvC4EMHk|Za1fBrNl!%EJNnCh>C>A z!*yS(KTmkPlu8azk4I;1krc~6uCnz-t>h5(xP&?8MpTTGC&Lf7shh_?SxO}*sV7UR zBZVXYAVUvG)vM(A?;t~el^Qs~xI1!FHG&U3J~P%-Kz*xK`1~OvYRve|IRv|9 z$h@w?5!3Zqqr3$3=5hd$+SAJYh7QgqUoGgYSh;crM0^tV3OoHt;lYqnrqD#cg9N8$$ zCxw0fu12s;FQJ!*1shB0G3X}63klf?2%7{GNT`kzkl3WS{)!Z$FFz`kkTs5kSc0P$ z36Vq|Qo8VCc5OvYSCH2m+nYMRzegRa8pw)`oJRO_Ra496*BY4%$F1Rif1`#sY%Og`hH_g~ zZP&~Jt{hj1+rr)7sQ&qFrF`X-+a`RG`dr>^FSQYyXGw_FuRvnE%EngZg6^x*0mOxz zB*Z4FMB*z|?FPoruqqdBJCxtOXhbg6B*Dfmr7x=x*`eyX71in@afcEKuD>K9mgs`S zPLUYs8eE{b2|cI;I{&iY3@W=MwSA9DX-ZESWTd*jA{AtE6fWh5Z6S0rAQ z)kCOB_?HUD{8RlY|F6=LHiMF~H<70p{?(c@i})iD^9g8u8IT!swRZao^ELy>vBec; zx?Zi_E=S(m__MVO#)Du23R7N~Wm%rBIdNNAX~1k} z=f@0kr87Tf5O*|Rvn8-v$XgM-q-O<`4%JYPa?CC5`~( z6$IxaX$j8ht_2tAIb-jN83GvYitQ=>>MmVl*DoYsxJwg>`KM*TSuGCxE4?qjIHpKJ zu{c&j9N6O66tah1toM`fis*pvTB3D7*GfVN0;3T}3;|dkjSJbyF3~;gP8Ly^mPm6} zq%)Pmg)e(X^R8=YxLoT8`s6NZn%>I&JFmAIbFD2+tyB3klWS|fxTSG=c!{Tv4>C4A zeAd(BQkmS0rWqGC%?zQM05T$N;c2C);~roCUXP`kNXHDVeI_0}L@I#Qk&c8afb6y# zSc?1EEB6c>ZMssF#x1(S;|WZUz*Nx(#JY8PEF z3Fz~tcIQr$fJSd>ckYw~3cRKLUd8yu*>M2FWK#wCaTVfQ-zu#_jQ5syVFx2Z6>mvT zJSej`DPHkitw)4Mf2z7kI}1kvBm0SSd{?_2EM*~kSNE||AfS=usm*l5gf5-oc@?^4 zY-h|MuCX&77UCK^Bd3%!mpz>_><(tw1=-|Bx@=3Bs?;O$A8BqyWZ21rGXLXf*ny}9 zXf*`F|8c|*g#Y7c*a6Y-<7n9Ne2mRgv@U^Q0!BwltoN-QG@1Qjr%l419WF1HH+xzziK?rp~?5T!oRye;lWqf^=B=elgWR`uO-$2YZ2 zp4og+Q(LYvJgiFB=0DfGh882p^E7{JWCRfP0d0LiRNopI0Z4sYBO?H*Z);?PlqmCs zb|DB5Ou&dq=H3_D-QeWOHl_R87z7ZiFEk+ts}x4qrd=c_WsCrfsKf}{w2S1B3)433 zA~`^qwrL_cBQQcbo7}0({*4hD{=*0-cG3Hvvr`k4kotHm$JQ${0tnTCwmu+6*cBN8 zNPW8^BLJyymlz?GVV@Rl_Jpgu=tcQGF@+3)Ju!teevjrNLOK}{DE7$uXO*Gx-)LTA z+DsV%z(^U17km>lNp;`oY@etwOy6k24ltb#GjK0!m&=vlDlnpwIk;E5s1RkL+N*oo zJ2s$^Y{hr~V+n?&VrMKp0@u`?Y z`uizA~l& zjQJ%3urlVCL9o)Rwy^_5M8+FoZl8q#{*~USR{k{7&#tm`*^3piw~Yl1uqe!)BZsLT(f2@+p;$v3R?%7OnFmN)2=@_*z_pg9vtIdRW_^f-&lRLw}|yyEF2ow5=J2io~XCiCkR&+RjT zRQ-zA!_EdE?S91@7UKvcKP$k6V6^TPc6UiHQqLxDkR2=Cz*Mx!JM@UmZdo@nno!7=@azNi z&hyKuWlLNOmyW=q2jJEbSZz;mh*8THkDEjBu|R6s;^hvGAeC(432i2C>L84=HC%R( z{?!E))Uh?Lqk=lN#&t+|Wvl1>ct&$M3FNlg)=@znTfG6+kCRIsTfKu1&D>@iLgZz8 z`0GLXH^){|$@aLCN-EhN4_1lDZ1=K}E32fE?b0psz$YWYn4RGngY}8ys$kg}vsA&d zGiH$>%}y_CS4+fT+39gDZw|B>O@!u~j`s)aE^1sCc)p2wy1?^I%p;+jZ(;}E1)gs_ zV!v{<&S=6m-+JNTq56t()zEw!(^NzAZA>Fkoo{1xs-gK-)OpU@hLFzH;ip6O?@y?K z=4uX?&ZafcT^EHBFfI!r zi4kEI!s}wfEQHtj*~r^uA-v8XJXBs#O8aWLk_)#D*Q<}Mg=TI{Qwz=9m`2`L&h?AS zU@bIreXJy*_KdD091%Wuj2?3$f5S5`=BdMf>KPom_b4`7Y89glC@cTxoc^5|r@u z5%n$MG3V<;!~Z^5A3LEdd?ZHA>k8j|-`(bBw7hJu`K)1XG&){i7-~7CQ7N%KcLciR;Wdl)b zDMZjfxdSWDChzpiw)*xNl1$n=ectfClc0s>ClLPfj-jLUdwSCHs8uAR>2lvqb@Q_c zV71*NJ0YJvvP?rD({Q=(R!Sh#aJkP)`Gs^TKc?WJXn4_KdfTKY;tFI(@`0#Q ztL^10BAD$7pVzFm0$vjW519}_DtN*_=J?EZTLGM_Q6^T-aS_ba#BsTcr$&I?#m~vSOH^g$X)hdO^8Q3X1&T=_fZ1VM~ z^{;v1J&k%xc*)Uv_assxy=CLJDb`!qHu=@o?<0-goBT0$K?Bm6oBX3rF?&OSCVT8e z;~UClTx=HKaLm}vaRstN+8lWW%22`P$SVM0*eqT_E{@CP)VIYCH%`(g9|u79EisY2 z6W9{D19BnS61f8)L|eoi$d#8Yr=R=g%4=e|TzP#Sc>)k}KW_w?DwXNU4yW@7KPbo$zdFbF=ofPndsyHnwDo27CD3OnvyP@w!hx z{qw9jyDSvM7SKpnhO(s?8$|~SaV_Gz=LTQ z6)=*MVM5Q;4V=X)lgU7TPUJa8x+Rn8;k5+#UA* zz8>6gcWF5><=v&_!1spDlpA<&X*p@_y;APqm8S8uV|79guHRAF@$C2YI!`r+-jTZR z*vZqHCjX2dfc<<{^US6xxv9-f)2FoMF5~z2%`F*X(bF4$d3mmF) zsiu{dg&)Du#BnXT<{6hvZ<@h(2auqF?ibzDT9H8C)^@4m|7$#2KC7*rhRE-~+nU?j z;m9>!gmrSw?P9)sWIy!FHMU$X+P61Po;AI3W~rrzh?1HAt%u=mqI+oNpvLK!HD2B} z$mzky75;Ze4#6I|K`oem=AcYetmgle3;P_G?3t&iX&0t(orsOwGPIG|d<|55U?=IPUOb}$Hu{1Cm76zOXDr6T5^+SoiDjqKEv)sYciggE5Yv1^dEZgs-l zlU112adm=wGAGNlUcFzU=b#8PuU97s;Xf*yk_uVU)`Wvk(~nPD6W1W?*qXQoS;y8S z++_+POt>`(HrKWWS;y8ST$GAjDp->si1eAQ0IK!j;iv1Z=d6z_kSNvqxB^+n)+gMq zr^2jb>l1F*1EhlW3AgJ3Qo;HJyB>L=ULij2weX5F^tef{#r$&2e=X*h@^2>sd&6A8wJA>ZorG_n zBNS0Cfz-z}98KTSd`Ax`$)QVH;T3Ew{Hs_^p!Y!D&E{y&T-Gr?O}+ct?=0Hd)f59c})?)fQY;p$BEiiV18aXJ&6@Rv)Brh$c6JGt@yqOS(+41MIbYGR56w3R@CfkVGQ1<a)0WJQMvTA?h3wkob(w$rmeOn0;OO6C)PRv4c%Bb*H#-&bJ|D;feODN!J&CHJ!_ASRLLz2+6Xs>#aR8Y1 z#!Tff?M=9N1GzBmO}Kai5T?Bece1G9vA(}A{BV*!?Wlb*1F!Y{eF+y~pbQNA5-!33 zgkfL89VsMw_V0u_QsgV-Nb&E43od|=1KJ}6kYVxf1V@VA97QS^6tk1MvY`vRff+2D zQ*u9TzxgUZf9A$SaveA~>8wkxDeJ5Y#JY2n&bksdoEHwCu7CW)N-^%d zq%$rEF`zXrkjBkRy7O99Hu;-m*(0uT5+?jjk~^OZQB6LO!29i=UZ5`;dwZl4iBxoZ z(w$MvmlA;0_O8`@Dgnr2n@?n!;~NM>bi(w7`qQ7>8B@q~xHG1ZX5E=|8v*m#1fbgp z0C78aCf!B=h~v65$woj<^;HawyTg+jb>CrkM?;{>41rQedky@ftUp!a(wBDJ(Wp;W zla|Ih$c(3+pA(lrM6W`x05rUy#V6Y#iIgI zbXBsiJtPBBbX9W5!Djw;kLffle$PJ zy%=>82r;1TBp@w%G3q3~%JFit?0wfs5?6mYNdQ%BXudYFE@@o4P0f^9EenPVi?}QtwXM8)z`&hXlI`0n#hGl5R5wr1D+K-Zm@;#0a~R z?nWGl5q2k&_G%1>5q2kQ`c;^5YQ6@t$BxtR>YwZ6*gbJI()oL$VMz`Vdez-(;|hLZ%x zDPNC}nL96_9dh(BMT`trR>cHJ=LNV?*<@u!&07OLk0c%u1R>Cl1t6VtYfx^#TLXma z)_~vUxWX{q8W3_c?|`#rsVanjxK!VhFT@<8Q6X?)%d8nsg}_}{0^ukG{FudRgV}En z+%X|*9zFphZN#_V9y5tGZV$5dG)iHZZVz}+08^f(02c)AAsL8<0V67T_OT#vk!f3ekmudkIM^EsU5XxUw)-N|x(|LEc>vQW~m-v9&48J5_sIo7M7Q z0vJ(A7$uCAlEYyb)I?n)*8!mn2CEc3b4TEwh-J-v05D=2B@vT5Vy)!Vct?=65e!O0 zbw|J(vG-+k^8uNqLD{z*)tBq{RDLPp4Rg|}6&Lo~KtCARm^&Dj-v)vGkpWP~&i#Qq zR2xhI66{ISB+#Gt2kxXP2^3fsxE}}@Y!mDNmV~^V<(CCr?3bG)1^l~G5#b;q7O^Et zBxv0O0pDVh5ibnVyCm2j9+?C<9|&-qa-x*a*4xaJ`}3G@po)|&3yIe~j9 z=DwX1xStOKsqUeGYsoImARrG99}0#%fz@QzN&>gu4+ri*D!3NAEic+l5>W7wz?V6i zM^94fDDr6FPq1x}1d2TN9|^SK@xUMN`U!hx8RT8`Nm3~BRNx~22HOOCTVfIrc`9(XC6WN{2;9$73`T+kdlh36ObM=H ztmV;qWuTuomN%al@`+WD5?Nl>qLq>Vv0oorDGpNn4-m_*lo??y&sqdq!}CSUo1elx z6I-4S)?n6Wt)a!vB%t8$OB3>9`Bh?9GkPc@mVYkr$2mh2-g_a?Yl?;zE4>f|@(VC} z+eZ2R5cmgLdBGGQ!8&bI02}-vz`H~e6nHVRT3$*>=aCYbjRZtq42;=qCE%ChNnxXW zBt(|7{FMT;U@+fC$ zw#~4OeYI$4Ekk^5Y-nu^{l|ESlK|to*wEUH@K@piO#&s>2l?WN_Oi*>0{v#u&@zi( z3j+D%o7lhv5?>GeoHMiyB$8l*QltA9+lK|ykV?%q!&~MB5wF8<2O1u+$4t~q{exNrx zJ2U=d341?qaciKs`45V93Z?)FHd$cde31imnB?)J1107pZ@s<=-}$xf zJLDVLL7OI-gFz`o!oZx9_($0PTHVm=AF+fC_EH!=a;+XU61jf{mG+XV*7PJWszf^B zpF!82nTItSGQMj!J5^TbxZ*lJSk=x>@m_uYT16R1Z%COhYPnCv%}wcpjdA%H9E3nSxa^nX=BC^alWw<59XJ(}P>=)#2N!8jNxBY_L zEh&y0b1VmTzb$;|Mt#H4x5X5)8r~LDh#78+DP%FcO(@=zuP^cyx;s;Rt>))*^}E$~ zrijp7Q-gNq3v@4coO6@DHZ}R>h}wRP?&Xv_P}z^sy_~AE7mYxuU$z|ph4l8?l)f`< z_upU?S)1aBd1FAQgT;kxO!-6V{}GP6UBB(OPM$ogqt62Uke)`e?Iu%N6BmsO!+8(Zh$|1SgH7@7~--IwN?(sTZYIZY4-y`BM8$YX<`}gM;0JqMY!K@b?1pI zN~vUItSF_Dk+CA}eh4U35b^rk+i+>W?m#C1q9k zn`Qcl{Hk=ayris{XjPi`pE7JkT}J8iX?Hv^*e2LvYZ7pW&!;QxNzo*r$n$C9@-l2q z0xf<%O-O0cMB@61ztbHPf9EafcYWn+9{>9Mo9L)~ JEqTh^{|6-(mPY^p delta 25415 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 j_V$ 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.