From 8a3733eaf7443d355bde28f28f2acb4a9da60712 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Fri, 2 Feb 2024 14:48:34 -0500 Subject: [PATCH] sct(component): implement `Component` trait and split modules (#3723) * sct(view): remove catch-all view module * sct(component): remove top-level module sig * sct(component): add `component::clock` I am not completely sold on the name, but the idea is that what you find here are extension traits that let you manage the blockchain clocks. Those clocks can be logical e.g. a block or an epoch, or literal wall clock (recorded by block timestamps, and eventually PBTS). In particular, we define `EpochManager` and `EpochRead` which implement write and read capabiltiies respectively. * sct(component): add `component::Tree` In this module, we find composable extension traits that let us interface with the chain's commitment tree. This is done by plugging into `SctRead` or `SctManager`. We also add a third interface (`tree:VerificationExt`) which action handlers can pull in order to perform SCT specific verification of anchors or nullifiers. * sct(component): move `SourceContext` This is a purely mechanical refactor, and might be better to merge into the tree module to increase cohesion. * sct(component): update query service imports * sct(component): implement `cnidarium_component::Component` on the sct * sct(component): propagate api changes * sct(component): sketch out updated state key schema * app: use `Sct::init_chain` and `Sct::begin_block` * sct(component): track module definition * sct(component): use updated state key schema * penumbra: misc small fixes --- Cargo.lock | 1 + .../pcli/src/command/query/shielded_pool.rs | 6 +- crates/bin/pd/src/migrate.rs | 2 +- .../app/src/action_handler/actions/submit.rs | 5 +- .../app/src/action_handler/transaction.rs | 2 +- .../action_handler/transaction/stateful.rs | 26 +- crates/core/app/src/app/mod.rs | 51 +-- crates/core/app/src/community_pool_ext.rs | 2 +- crates/core/app/src/mock_client.rs | 4 +- crates/core/app/src/penumbra_host_chain.rs | 2 +- crates/core/app/src/tests/spend.rs | 2 +- .../core/app/src/tests/swap_and_swap_claim.rs | 6 +- .../compact-block/src/component/manager.rs | 6 +- .../compact-block/src/component/rpc.rs | 2 +- .../dex/src/component/action_handler/swap.rs | 2 +- .../component/action_handler/swap_claim.rs | 8 +- .../core/component/dex/src/component/arb.rs | 2 +- .../core/component/dex/src/component/dex.rs | 4 +- .../dex/src/component/swap_manager.rs | 2 +- .../core/component/dex/src/component/tests.rs | 2 +- .../component/distributions/src/component.rs | 4 +- .../component/governance/src/component.rs | 2 +- .../governance/src/component/view.rs | 2 +- .../component/ibc/src/component/client.rs | 2 +- crates/core/component/sct/Cargo.toml | 4 +- crates/core/component/sct/src/component.rs | 8 - .../core/component/sct/src/component/clock.rs | 100 +++++ .../core/component/sct/src/component/mod.rs | 20 + .../core/component/sct/src/component/rpc.rs | 9 +- .../core/component/sct/src/component/sct.rs | 110 ++++++ .../component/sct/src/component/source.rs | 30 ++ .../core/component/sct/src/component/tree.rs | 241 ++++++++++++ .../core/component/sct/src/component/view.rs | 353 ------------------ crates/core/component/sct/src/state_key.rs | 69 ++-- .../src/component/action_handler/output.rs | 2 +- .../src/component/action_handler/spend.rs | 5 +- .../src/component/note_manager.rs | 4 +- .../src/component/shielded_pool.rs | 2 +- .../src/action_handler/undelegate_claim.rs | 4 +- .../action_handler/validator_definition.rs | 4 +- crates/core/component/stake/src/component.rs | 14 +- crates/view/src/worker.rs | 2 +- 42 files changed, 631 insertions(+), 497 deletions(-) delete mode 100644 crates/core/component/sct/src/component.rs create mode 100644 crates/core/component/sct/src/component/clock.rs create mode 100644 crates/core/component/sct/src/component/mod.rs create mode 100644 crates/core/component/sct/src/component/sct.rs create mode 100644 crates/core/component/sct/src/component/source.rs create mode 100644 crates/core/component/sct/src/component/tree.rs delete mode 100644 crates/core/component/sct/src/component/view.rs diff --git a/Cargo.lock b/Cargo.lock index c5c361a389..5b29371156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5550,6 +5550,7 @@ dependencies = [ "blake2b_simd 0.5.11", "bytes", "cnidarium", + "cnidarium-component", "decaf377 0.5.0", "decaf377-rdsa", "hex", diff --git a/crates/bin/pcli/src/command/query/shielded_pool.rs b/crates/bin/pcli/src/command/query/shielded_pool.rs index e9ae661ad1..68f8f81b2d 100644 --- a/crates/bin/pcli/src/command/query/shielded_pool.rs +++ b/crates/bin/pcli/src/command/query/shielded_pool.rs @@ -31,13 +31,13 @@ impl ShieldedPool { pub fn key(&self) -> String { use penumbra_sct::state_key as sct_state_key; match self { - ShieldedPool::Anchor { height } => sct_state_key::anchor_by_height(*height), + ShieldedPool::Anchor { height } => sct_state_key::tree::anchor_by_height(*height), ShieldedPool::CompactBlock { .. } => { unreachable!("should be handled at outer level via rpc"); } - ShieldedPool::Commitment { commitment } => sct_state_key::note_source(commitment), + ShieldedPool::Commitment { commitment } => sct_state_key::tree::note_source(commitment), ShieldedPool::Nullifier { nullifier } => { - sct_state_key::spent_nullifier_lookup(nullifier) + sct_state_key::nullifier_set::spent_nullifier_lookup(nullifier) } } } diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index 99d2513727..8c05e1ce05 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use cnidarium::{StateDelta, StateWrite, Storage}; use jmt::RootHash; use penumbra_app::{genesis, SUBSTORE_PREFIXES}; -use penumbra_sct::component::{EpochManager, EpochRead}; +use penumbra_sct::component::clock::{EpochManager, EpochRead}; use penumbra_stake::{genesis::Content as StakeContent, StateReadExt as _}; use crate::testnet::generate::TestnetConfig; diff --git a/crates/core/app/src/action_handler/actions/submit.rs b/crates/core/app/src/action_handler/actions/submit.rs index 4adf342e09..240b511ebe 100644 --- a/crates/core/app/src/action_handler/actions/submit.rs +++ b/crates/core/app/src/action_handler/actions/submit.rs @@ -14,7 +14,8 @@ use penumbra_community_pool::component::StateReadExt as _; use penumbra_ibc::component::ClientStateReadExt; use penumbra_keys::keys::{FullViewingKey, NullifierKey}; use penumbra_proto::DomainType; -use penumbra_sct::component::{EpochRead, StateReadExt as _}; +use penumbra_sct::component::clock::EpochRead; +use penumbra_sct::component::tree::SctRead; use penumbra_shielded_pool::component::SupplyWrite; use penumbra_transaction::plan::TransactionPlan; @@ -293,7 +294,7 @@ impl ActionHandler for ProposalSubmit { // Compute the effective starting TCT position for the proposal, by rounding the current // position down to the start of the block. - let Some(sct_position) = state.state_commitment_tree().await.position() else { + let Some(sct_position) = state.get_sct().await.position() else { anyhow::bail!("state commitment tree is full"); }; // All proposals start are considered to start at the beginning of the block, because this diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index a6ea573ec8..1131256454 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; -use penumbra_sct::{component::SourceContext as _, CommitmentSource}; +use penumbra_sct::{component::source::SourceContext, CommitmentSource}; use penumbra_transaction::Transaction; use tokio::task::JoinSet; use tracing::{instrument, Instrument}; diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index 36f6178358..074e3e1483 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -1,23 +1,16 @@ use anyhow::Result; use cnidarium::StateRead; use penumbra_fee::component::StateReadExt as _; -use penumbra_sct::component::{EpochRead, StateReadExt as _}; +use penumbra_sct::component::clock::EpochRead; +use penumbra_sct::component::tree::VerificationExt; use penumbra_shielded_pool::component::StateReadExt as _; use penumbra_shielded_pool::fmd; use penumbra_transaction::gas::GasCost; use penumbra_transaction::Transaction; -pub(super) async fn claimed_anchor_is_valid( - state: S, - transaction: &Transaction, -) -> Result<()> { - state.check_claimed_anchor(transaction.anchor).await -} +const FMD_GRACE_PERIOD_BLOCKS: u64 = 10; -pub(super) async fn fmd_parameters_valid( - state: S, - transaction: &Transaction, -) -> Result<()> { +pub async fn fmd_parameters_valid(state: S, transaction: &Transaction) -> Result<()> { let previous_fmd_parameters = state .get_previous_fmd_parameters() .await @@ -35,8 +28,6 @@ pub(super) async fn fmd_parameters_valid( ) } -const FMD_GRACE_PERIOD_BLOCKS: u64 = 10; - pub fn fmd_precision_within_grace_period( tx: &Transaction, previous_fmd_parameters: fmd::Parameters, @@ -64,7 +55,14 @@ pub fn fmd_precision_within_grace_period( Ok(()) } -pub(super) async fn fee_greater_than_base_fee( +pub async fn claimed_anchor_is_valid( + state: S, + transaction: &Transaction, +) -> Result<()> { + state.check_claimed_anchor(transaction.anchor).await +} + +pub async fn fee_greater_than_base_fee( state: S, transaction: &Transaction, ) -> Result<()> { diff --git a/crates/core/app/src/app/mod.rs b/crates/core/app/src/app/mod.rs index 7d6281a84e..3544fc8e3c 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -20,7 +20,9 @@ use penumbra_ibc::component::{Ibc, StateWriteExt as _}; use penumbra_ibc::StateReadExt as _; use penumbra_proto::core::app::v1alpha1::TransactionsByHeightResponse; use penumbra_proto::DomainType; -use penumbra_sct::component::{EpochManager, EpochRead, SctParameterWriter, StateReadExt as _}; +use penumbra_sct::component::clock::EpochRead; +use penumbra_sct::component::sct::Sct; +use penumbra_sct::component::{StateReadExt as _, StateWriteExt as _}; use penumbra_sct::epoch::Epoch; use penumbra_shielded_pool::component::{ShieldedPool, StateReadExt as _, StateWriteExt as _}; use penumbra_stake::component::{Staking, StateReadExt as _, StateWriteExt as _, ValidatorUpdates}; @@ -98,29 +100,7 @@ impl App { match app_state { genesis::AppState::Content(genesis) => { state_tx.put_chain_id(genesis.chain_id.clone()); - state_tx.put_sct_params(genesis.sct_content.sct_params.clone()); // TODO(erwan): promote Sct to component? - - // The genesis block height is 0 - state_tx.put_block_height(0); - - state_tx.put_epoch_by_height( - 0, - Epoch { - index: 0, - start_height: 0, - }, - ); - - // We need to set the epoch for the first block as well, since we set - // the epoch by height in end_block, and end_block isn't called after init_chain. - state_tx.put_epoch_by_height( - 1, - Epoch { - index: 0, - start_height: 0, - }, - ); - + Sct::init_chain(&mut state_tx, Some(&genesis.sct_content)).await; ShieldedPool::init_chain(&mut state_tx, Some(&genesis.shielded_pool_content)).await; Distributions::init_chain(&mut state_tx, Some(&genesis.distributions_content)) .await; @@ -219,11 +199,6 @@ impl App { pub async fn begin_block(&mut self, begin_block: &request::BeginBlock) -> Vec { let mut state_tx = StateDelta::new(self.state.clone()); - // store the block height - state_tx.put_block_height(begin_block.header.height.into()); - // store the block time - state_tx.put_block_timestamp(begin_block.header.time); - // If a app parameter change is scheduled for this block, apply it here, before any other // component has executed. This ensures that app parameter changes are consistently // applied precisely at the boundary between blocks: @@ -266,6 +241,7 @@ impl App { // Run each of the begin block handlers for each component, in sequence: let mut arc_state_tx = Arc::new(state_tx); + Sct::begin_block(&mut arc_state_tx, begin_block).await; ShieldedPool::begin_block(&mut arc_state_tx, begin_block).await; Distributions::begin_block(&mut arc_state_tx, begin_block).await; Ibc::begin_block::>>>( @@ -461,17 +437,17 @@ impl App { .await .expect("able to get block height in end_block"); let current_epoch = state_tx - .current_epoch() + .get_current_epoch() .await .expect("able to get current epoch in end_block"); let is_end_epoch = current_epoch.is_scheduled_epoch_end( current_height, state_tx - .get_epoch_duration() + .get_epoch_duration_parameter() .await .expect("able to get epoch duration in end_block"), - ) || state_tx.epoch_ending_early(); + ) || state_tx.is_epoch_ending_early().await; // If a chain upgrade is scheduled for this block, we trigger an early epoch change // so that the upgraded chain starts at a clean epoch boundary. @@ -522,7 +498,8 @@ impl App { .expect("must be able to finish compact block"); // set the epoch for the next block - state_tx.put_epoch_by_height( + penumbra_sct::component::clock::EpochManager::put_epoch_by_height( + &mut state_tx, current_height + 1, Epoch { index: current_epoch.index + 1, @@ -533,7 +510,11 @@ impl App { self.apply(state_tx) } else { // set the epoch for the next block - state_tx.put_epoch_by_height(current_height + 1, current_epoch); + penumbra_sct::component::clock::EpochManager::put_epoch_by_height( + &mut state_tx, + current_height + 1, + current_epoch, + ); state_tx .finish_block(state_tx.app_params_updated()) @@ -723,7 +704,7 @@ impl< + penumbra_governance::component::StateReadExt + penumbra_fee::component::StateReadExt + penumbra_community_pool::component::StateReadExt - + penumbra_sct::component::EpochRead + + penumbra_sct::component::clock::EpochRead + penumbra_ibc::component::StateReadExt + penumbra_distributions::component::StateReadExt + ?Sized, diff --git a/crates/core/app/src/community_pool_ext.rs b/crates/core/app/src/community_pool_ext.rs index 57079b37f5..080f3b27f3 100644 --- a/crates/core/app/src/community_pool_ext.rs +++ b/crates/core/app/src/community_pool_ext.rs @@ -5,7 +5,7 @@ use cnidarium::{StateRead, StateWrite}; use futures::{StreamExt, TryStreamExt}; use penumbra_governance::state_key; use penumbra_proto::{StateReadProto, StateWriteProto}; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use penumbra_transaction::Transaction; // Note: These should live in `penumbra-governance` in the `StateReadExt` and `StateWriteExt` diff --git a/crates/core/app/src/mock_client.rs b/crates/core/app/src/mock_client.rs index 9784376629..761a431f31 100644 --- a/crates/core/app/src/mock_client.rs +++ b/crates/core/app/src/mock_client.rs @@ -4,7 +4,7 @@ use cnidarium::StateRead; use penumbra_compact_block::{component::StateReadExt as _, CompactBlock, StatePayload}; use penumbra_dex::swap::SwapPlaintext; use penumbra_keys::FullViewingKey; -use penumbra_sct::component::StateReadExt as _; +use penumbra_sct::component::tree::SctRead; use penumbra_shielded_pool::{note, Note}; use penumbra_tct as tct; @@ -42,7 +42,7 @@ impl MockClient { let (latest_height, root) = self.latest_height_and_sct_root(); anyhow::ensure!(latest_height == height, "latest height should be updated"); let expected_root = state - .anchor_by_height(height) + .get_anchor_by_height(height) .await? .ok_or_else(|| anyhow::anyhow!("missing sct anchor for height {}", height))?; anyhow::ensure!( diff --git a/crates/core/app/src/penumbra_host_chain.rs b/crates/core/app/src/penumbra_host_chain.rs index b0ec1556f1..0fc8cf53a8 100644 --- a/crates/core/app/src/penumbra_host_chain.rs +++ b/crates/core/app/src/penumbra_host_chain.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use penumbra_ibc::component::HostInterface; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use crate::app::StateReadExt; diff --git a/crates/core/app/src/tests/spend.rs b/crates/core/app/src/tests/spend.rs index 654c11fda3..f9208b226d 100644 --- a/crates/core/app/src/tests/spend.rs +++ b/crates/core/app/src/tests/spend.rs @@ -9,7 +9,7 @@ use penumbra_compact_block::component::CompactBlockManager; use penumbra_keys::{test_keys, PayloadKey}; use penumbra_num::Amount; use penumbra_sct::{ - component::{EpochManager, SourceContext}, + component::{clock::EpochManager, source::SourceContext}, epoch::Epoch, }; use penumbra_shielded_pool::{component::ShieldedPool, SpendPlan}; diff --git a/crates/core/app/src/tests/swap_and_swap_claim.rs b/crates/core/app/src/tests/swap_and_swap_claim.rs index a01faf097b..f653449e1f 100644 --- a/crates/core/app/src/tests/swap_and_swap_claim.rs +++ b/crates/core/app/src/tests/swap_and_swap_claim.rs @@ -1,7 +1,7 @@ use ark_ff::UniformRand; use penumbra_compact_block::component::CompactBlockManager as _; use penumbra_sct::{ - component::{EpochManager, EpochRead, SourceContext as _}, + component::{clock::EpochManager, source::SourceContext as _, StateReadExt as _}, epoch::Epoch, }; use std::{ops::Deref, sync::Arc}; @@ -94,7 +94,7 @@ async fn swap_and_swap_claim() -> anyhow::Result<()> { // To do this, we need to have an auth path for the swap nft note, which // means we have to synchronize a client's view of the test chain's SCT // state. - let epoch_duration = state.get_epoch_duration().await?; + let epoch_duration = state.get_epoch_duration_parameter().await?; let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone()); // TODO: generalize StateRead/StateWrite impls from impl for &S to impl for Deref client.sync_to(1, state.deref()).await?; @@ -205,7 +205,7 @@ async fn swap_claim_duplicate_nullifier_previous_transaction() { state_tx.apply(); // 6. Create a SwapClaim action - let epoch_duration = state.get_epoch_duration().await.unwrap(); + let epoch_duration = state.get_epoch_duration_parameter().await.unwrap(); let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone()); client.sync_to(1, state.deref()).await.unwrap(); diff --git a/crates/core/component/compact-block/src/component/manager.rs b/crates/core/component/compact-block/src/component/manager.rs index f4ce718b55..bb556764c0 100644 --- a/crates/core/component/compact-block/src/component/manager.rs +++ b/crates/core/component/compact-block/src/component/manager.rs @@ -7,9 +7,9 @@ use penumbra_dex::component::SwapManager as _; use penumbra_fee::component::StateReadExt as _; use penumbra_governance::StateReadExt as _; use penumbra_proto::DomainType; -use penumbra_sct::component::EpochRead; -use penumbra_sct::component::SctManager as _; -use penumbra_sct::component::StateReadExt as _; +use penumbra_sct::component::clock::EpochRead; +use penumbra_sct::component::tree::SctManager as _; +use penumbra_sct::component::tree::SctRead; use penumbra_shielded_pool::component::NoteManager as _; use tracing::instrument; diff --git a/crates/core/component/compact-block/src/component/rpc.rs b/crates/core/component/compact-block/src/component/rpc.rs index 499baf3036..9a15ddc6fa 100644 --- a/crates/core/component/compact-block/src/component/rpc.rs +++ b/crates/core/component/compact-block/src/component/rpc.rs @@ -7,7 +7,7 @@ use penumbra_proto::core::component::compact_block::v1alpha1::{ query_service_server::QueryService, CompactBlockRangeRequest, CompactBlockRangeResponse, CompactBlockRequest, CompactBlockResponse, }; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use tokio::sync::mpsc; use tonic::Status; use tracing::{instrument, Instrument}; diff --git a/crates/core/component/dex/src/component/action_handler/swap.rs b/crates/core/component/dex/src/component/action_handler/swap.rs index 0bb3cdb053..fd5c87b7a1 100644 --- a/crates/core/component/dex/src/component/action_handler/swap.rs +++ b/crates/core/component/dex/src/component/action_handler/swap.rs @@ -6,7 +6,7 @@ use cnidarium::{StateRead, StateWrite}; use cnidarium_component::ActionHandler; use penumbra_proof_params::SWAP_PROOF_VERIFICATION_KEY; use penumbra_proto::StateWriteProto; -use penumbra_sct::component::SourceContext as _; +use penumbra_sct::component::source::SourceContext; use crate::{ component::{metrics, StateReadExt, StateWriteExt, SwapManager}, diff --git a/crates/core/component/dex/src/component/action_handler/swap_claim.rs b/crates/core/component/dex/src/component/action_handler/swap_claim.rs index 7611579421..18918240f6 100644 --- a/crates/core/component/dex/src/component/action_handler/swap_claim.rs +++ b/crates/core/component/dex/src/component/action_handler/swap_claim.rs @@ -8,7 +8,11 @@ use penumbra_txhash::TransactionContext; use cnidarium::{StateRead, StateWrite}; use penumbra_proof_params::SWAPCLAIM_PROOF_VERIFICATION_KEY; use penumbra_proto::StateWriteProto; -use penumbra_sct::component::{EpochRead, SctManager as _, SourceContext, StateReadExt as _}; +use penumbra_sct::component::{ + source::SourceContext, + tree::{SctManager, VerificationExt}, + StateReadExt as _, +}; use penumbra_shielded_pool::component::NoteManager; use crate::{ @@ -43,7 +47,7 @@ impl ActionHandler for SwapClaim { // 1. Validate the epoch duration passed in the swap claim matches // what we know. - let epoch_duration = state.get_epoch_duration().await?; + let epoch_duration = state.get_epoch_duration_parameter().await?; let provided_epoch_duration = swap_claim.epoch_duration; if epoch_duration != provided_epoch_duration { anyhow::bail!("provided epoch duration does not match chain epoch duration"); diff --git a/crates/core/component/dex/src/component/arb.rs b/crates/core/component/dex/src/component/arb.rs index bbfcc81f10..8f3b3006e3 100644 --- a/crates/core/component/dex/src/component/arb.rs +++ b/crates/core/component/dex/src/component/arb.rs @@ -4,7 +4,7 @@ use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateDelta, StateWrite}; use penumbra_asset::{asset, Value}; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use tracing::instrument; use crate::{ExecutionCircuitBreaker, SwapExecution}; diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index 96216496a0..19025973a4 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -6,7 +6,7 @@ use cnidarium::{StateRead, StateWrite}; use cnidarium_component::Component; use penumbra_asset::{asset, STAKING_TOKEN_ASSET_ID}; use penumbra_proto::{StateReadProto, StateWriteProto}; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use tendermint::v0_37::abci; use tracing::instrument; @@ -41,7 +41,7 @@ impl Component for Dex { state: &mut Arc, end_block: &abci::request::EndBlock, ) { - let current_epoch = state.current_epoch().await.expect("epoch is set"); + let current_epoch = state.get_current_epoch().await.expect("epoch is set"); // For each batch swap during the block, calculate clearing prices and set in the JMT. for (trading_pair, swap_flows) in state.swap_flows() { diff --git a/crates/core/component/dex/src/component/swap_manager.rs b/crates/core/component/dex/src/component/swap_manager.rs index c3b8bc50d2..901f432b84 100644 --- a/crates/core/component/dex/src/component/swap_manager.rs +++ b/crates/core/component/dex/src/component/swap_manager.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use cnidarium::StateWrite; -use penumbra_sct::{component::SctManager as _, CommitmentSource}; +use penumbra_sct::{component::tree::SctManager, CommitmentSource}; use penumbra_tct as tct; use tracing::instrument; diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 83ad9a7563..9a9ab02291 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -33,7 +33,7 @@ pub trait TempStorageExt: Sized { #[async_trait] impl TempStorageExt for TempStorage { async fn apply_minimal_genesis(self) -> anyhow::Result { - use penumbra_sct::component::EpochManager as _; + use penumbra_sct::component::clock::EpochManager as _; let mut state = StateDelta::new(self.latest_snapshot()); state.put_block_height(0); diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index e4bbfcba63..02eefce546 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -59,10 +59,10 @@ impl Component for Distributions { trait DistributionManager: StateWriteExt { /// Compute the total new issuance of staking tokens for this epoch. async fn compute_new_issuance(&self) -> Result { - use penumbra_sct::component::EpochRead; + use penumbra_sct::component::clock::EpochRead; let current_block_height = self.get_block_height().await?; - let current_epoch = self.current_epoch().await?; + let current_epoch = self.get_current_epoch().await?; let num_blocks = current_block_height .checked_sub(current_epoch.start_height) .unwrap_or_else(|| panic!("epoch start height is less than or equal to current block height (epoch_start={}, current_height={}", current_epoch.start_height, current_block_height)); diff --git a/crates/core/component/governance/src/component.rs b/crates/core/component/governance/src/component.rs index 42b090cd90..a5b1bd6b6d 100644 --- a/crates/core/component/governance/src/component.rs +++ b/crates/core/component/governance/src/component.rs @@ -23,7 +23,7 @@ pub mod rpc; pub use view::StateReadExt; pub use view::StateWriteExt; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; pub struct Governance {} diff --git a/crates/core/component/governance/src/component/view.rs b/crates/core/component/governance/src/component/view.rs index ddccef51c3..637699c2ba 100644 --- a/crates/core/component/governance/src/component/view.rs +++ b/crates/core/component/governance/src/component/view.rs @@ -14,7 +14,7 @@ use penumbra_ibc::component::ClientStateWriteExt as _; use penumbra_num::Amount; use penumbra_proto::{StateReadProto, StateWriteProto}; use penumbra_sct::{ - component::{EpochRead, StateReadExt as _}, + component::{clock::EpochRead, tree::SctRead}, Nullifier, }; use penumbra_shielded_pool::component::SupplyRead; diff --git a/crates/core/component/ibc/src/component/client.rs b/crates/core/component/ibc/src/component/client.rs index 7fb9ef62fa..52f991d1db 100644 --- a/crates/core/component/ibc/src/component/client.rs +++ b/crates/core/component/ibc/src/component/client.rs @@ -369,7 +369,7 @@ mod tests { use cnidarium::{ArcStateDeltaExt, StateDelta}; use ibc_types::core::client::msgs::MsgUpdateClient; use ibc_types::{core::client::msgs::MsgCreateClient, DomainType}; - use penumbra_sct::component::{EpochManager as _, EpochRead}; + use penumbra_sct::component::clock::{EpochManager as _, EpochRead}; use std::str::FromStr; use tendermint::Time; diff --git a/crates/core/component/sct/Cargo.toml b/crates/core/component/sct/Cargo.toml index 9f247fb81a..2e5dc9f511 100644 --- a/crates/core/component/sct/Cargo.toml +++ b/crates/core/component/sct/Cargo.toml @@ -1,5 +1,4 @@ [package] -# TODO: merge with tct crate under a `component` feature flag? name = "penumbra-sct" version = "0.65.0-alpha.1" edition = "2021" @@ -9,7 +8,9 @@ edition = "2021" [features] component = [ "cnidarium", + "cnidarium-component", "penumbra-proto/cnidarium", + "penumbra-proto/rpc", "tonic", ] default = ["std", "component"] @@ -19,6 +20,7 @@ docsrs = [] [dependencies] # Workspace dependencies cnidarium = { path = "../../../cnidarium", optional = true } +cnidarium-component = { path = "../../../cnidarium-component", optional = true } penumbra-proto = { path = "../../../proto", default-features = false } penumbra-tct = { path = "../../../crypto/tct" } penumbra-keys = { path = "../../../core/keys", default-features = false } diff --git a/crates/core/component/sct/src/component.rs b/crates/core/component/sct/src/component.rs deleted file mode 100644 index 0bc414d4b3..0000000000 --- a/crates/core/component/sct/src/component.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod view; - -// TODO(erwan): this is a catch-all module that should be split up. -pub use view::{ - EpochManager, EpochRead, SctManager, SctParameterWriter, SourceContext, StateReadExt, -}; - -pub mod rpc; diff --git a/crates/core/component/sct/src/component/clock.rs b/crates/core/component/sct/src/component/clock.rs new file mode 100644 index 0000000000..82ecea9941 --- /dev/null +++ b/crates/core/component/sct/src/component/clock.rs @@ -0,0 +1,100 @@ +use crate::{epoch::Epoch, state_key}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use std::str::FromStr; + +#[async_trait] +/// Provides read access to epoch indices, block heights, timestamps, and other related data. +pub trait EpochRead: StateRead { + /// Get the current block height. + /// + /// # Errors + /// Returns an error if the block height is missing. + async fn get_block_height(&self) -> Result { + self.get_proto(state_key::block_manager::block_height()) + .await? + .ok_or_else(|| anyhow!("Missing block_height")) + } + + /// Gets the current block timestamp from the JMT + /// + /// # Errors + /// Returns an error if the block timestamp is missing. + /// + /// # Panic + /// Panics if the block timestamp is not a valid RFC3339 time string. + async fn get_block_timestamp(&self) -> Result { + let timestamp_string: String = self + .get_proto(state_key::block_manager::block_timestamp()) + .await? + .ok_or_else(|| anyhow!("Missing block_timestamp"))?; + + Ok(tendermint::Time::from_str(×tamp_string) + .context("block_timestamp was an invalid RFC3339 time string")?) + } + + /// Get the current application epoch. + /// + /// # Errors + /// Returns an error if the epoch is missing. + async fn get_current_epoch(&self) -> Result { + // Get the height + let height = self.get_block_height().await?; + + self.get(&state_key::epoch_manager::epoch_by_height(height)) + .await? + .ok_or_else(|| anyhow!("missing epoch for current height: {height}")) + } + + /// Get the epoch corresponding to the supplied height. + /// + /// # Errors + /// Returns an error if the epoch is missing. + async fn get_epoch_by_height(&self, height: u64) -> Result { + self.get(&state_key::epoch_manager::epoch_by_height(height)) + .await? + .ok_or_else(|| anyhow!("missing epoch for height")) + } + + /// Returns true if we are triggering an early epoch end. + async fn is_epoch_ending_early(&self) -> bool { + self.object_get(state_key::epoch_manager::end_epoch_early()) + .unwrap_or(false) + } +} + +impl EpochRead for T {} + +/// Provides write access to the chain's epoch manager. +/// The epoch manager is responsible for tracking block and epoch heights +/// as well as related data like reported timestamps and epoch duration. +#[async_trait] +pub trait EpochManager: StateWrite { + /// Writes the block timestamp as an RFC3339 string to verifiable storage. + fn put_block_timestamp(&mut self, timestamp: tendermint::Time) { + self.put_proto( + state_key::block_manager::block_timestamp().into(), + timestamp.to_rfc3339(), + ) + } + + /// Write a value in the end epoch flag in object-storage. + /// This is used to trigger an early epoch end at the end of the block. + fn set_end_epoch_flag(&mut self) { + self.object_put(state_key::epoch_manager::end_epoch_early(), true) + } + + /// Writes the block height to verifiable storage. + fn put_block_height(&mut self, height: u64) { + self.put_proto(state_key::block_manager::block_height().to_string(), height) + } + + /// Index the current epoch by height. + fn put_epoch_by_height(&mut self, height: u64, epoch: Epoch) { + self.put(state_key::epoch_manager::epoch_by_height(height), epoch) + } +} + +impl EpochManager for T {} diff --git a/crates/core/component/sct/src/component/mod.rs b/crates/core/component/sct/src/component/mod.rs new file mode 100644 index 0000000000..e7d09bdf07 --- /dev/null +++ b/crates/core/component/sct/src/component/mod.rs @@ -0,0 +1,20 @@ +//! The Sct component contains the interface to the chain's state commitment tree +//! and nullifier set. It also serves as tracking the various chain clocks, whether +//! logical, like an epoch index, or a block height, or physical, like block timestamps. + +/// Blockchain clocks: epoch indices, block heights and timestamps. +pub mod clock; +/// Implementation of the SCT component query server. +pub mod rpc; +/// The SCT component implementation. +pub mod sct; +/// Tracking commitment sources within a block execution. +pub mod source; +/// Mediate access to the state commitment tree and related data. +pub mod tree; + +// Access to configuration data for the component. +pub use sct::{StateReadExt, StateWriteExt}; + +// Note: for some reason, `rust-analyzer` chokes when this file is named +// `component.rs`. If you read this and manage to fix it, please rename it. diff --git a/crates/core/component/sct/src/component/rpc.rs b/crates/core/component/sct/src/component/rpc.rs index 7b9c779a4b..2f66be4331 100644 --- a/crates/core/component/sct/src/component/rpc.rs +++ b/crates/core/component/sct/src/component/rpc.rs @@ -1,11 +1,10 @@ use cnidarium::Storage; -use penumbra_proto::core::component::sct::v1alpha1::{ - query_service_server::QueryService, EpochByHeightRequest, EpochByHeightResponse, -}; +use penumbra_proto::core::component::sct::v1alpha1::query_service_server::QueryService; +use penumbra_proto::core::component::sct::v1alpha1::{EpochByHeightRequest, EpochByHeightResponse}; use tonic::Status; use tracing::instrument; -use super::EpochRead; +use super::clock::EpochRead; // TODO: Hide this and only expose a Router? pub struct Server { @@ -28,7 +27,7 @@ impl QueryService for Server { let state = self.storage.latest_snapshot(); let epoch = state - .epoch_by_height(request.get_ref().height) + .get_epoch_by_height(request.get_ref().height) .await .map_err(|e| tonic::Status::unknown(format!("could not get epoch for height: {e}")))?; diff --git a/crates/core/component/sct/src/component/sct.rs b/crates/core/component/sct/src/component/sct.rs new file mode 100644 index 0000000000..ed7bf58a7c --- /dev/null +++ b/crates/core/component/sct/src/component/sct.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use cnidarium_component::Component; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use tendermint::v0_37::abci; +use tracing::instrument; + +use crate::{epoch::Epoch, genesis, params::SctParameters, state_key}; + +use super::clock::EpochManager; + +pub struct Sct {} + +#[async_trait] +impl Component for Sct { + type AppState = genesis::Content; + + #[instrument(name = "sct_component", skip(state, app_state))] + async fn init_chain(mut state: S, app_state: Option<&Self::AppState>) { + match app_state { + Some(genesis) => { + state.put_sct_params(genesis.sct_params.clone()); + state.put_block_height(0); + state.put_epoch_by_height( + 0, + Epoch { + index: 0, + start_height: 0, + }, + ); + + // We need to set the epoch for the first block as well, since we set + // the epoch by height in end_block, and end_block isn't called after init_chain. + state.put_epoch_by_height( + 1, + Epoch { + index: 0, + start_height: 0, + }, + ); + } + None => { /* no-op until an upgrade occurs */ } + } + } + + #[instrument(name = "sct_component", skip(state, begin_block))] + async fn begin_block( + state: &mut Arc, + begin_block: &abci::request::BeginBlock, + ) { + let state = Arc::get_mut(state).expect("there's only one reference to the state"); + state.put_block_height(begin_block.header.height.into()); + state.put_block_timestamp(begin_block.header.time); + } + + #[instrument(name = "sct_component", skip(_state, _end_block))] + async fn end_block( + _state: &mut Arc, + _end_block: &abci::request::EndBlock, + ) { + } + + #[instrument(name = "sct_component", skip(_state))] + async fn end_epoch(_state: &mut Arc) -> anyhow::Result<()> { + Ok(()) + } +} + +/// This trait provides read access to configuration data for the component. +#[async_trait] +pub trait StateReadExt: StateRead { + /// Gets the fee parameters from the JMT. + async fn get_sct_params(&self) -> Result { + self.get(state_key::config::sct_params()) + .await? + .ok_or_else(|| anyhow!("Missing SctParameters")) + } + + /// Indicates if the sct parameters have been updated in this block. + fn sct_params_updated(&self) -> bool { + self.object_get::<()>(state_key::config::sct_params_updated()) + .is_some() + } + + /// Fetch the epoch duration parameter (measured in blocks). + /// + /// # Errors + /// Returns an error if the Sct parameters are missing. + async fn get_epoch_duration_parameter(&self) -> Result { + self.get_sct_params() + .await + .map(|params| params.epoch_duration) + } +} + +impl StateReadExt for T {} + +/// This trait provides write access to configuration data for the component. +#[async_trait] +pub trait StateWriteExt: StateWrite { + fn put_sct_params(&mut self, params: SctParameters) { + self.put(state_key::config::sct_params().to_string(), params); + self.object_put(state_key::config::sct_params_updated(), ()) + } +} + +impl StateWriteExt for T {} diff --git a/crates/core/component/sct/src/component/source.rs b/crates/core/component/sct/src/component/source.rs new file mode 100644 index 0000000000..0a94977d50 --- /dev/null +++ b/crates/core/component/sct/src/component/source.rs @@ -0,0 +1,30 @@ +use async_trait::async_trait; +use cnidarium::StateWrite; + +use crate::{state_key, CommitmentSource}; + +/// A helper trait for placing a `CommitmentSource` as ambient context during execution. +#[async_trait] +pub trait SourceContext: StateWrite { + fn put_current_source(&mut self, source: Option) { + if let Some(source) = source { + self.object_put(state_key::ambient::current_source(), source) + } else { + self.object_delete(state_key::ambient::current_source()) + } + } + + fn get_current_source(&self) -> Option { + self.object_get(state_key::ambient::current_source()) + } + + /// Sets a mock source, for testing. + /// + /// The `counter` field allows distinguishing hashes at different stages of the test. + fn put_mock_source(&mut self, counter: u8) { + self.put_current_source(Some(CommitmentSource::Transaction { + id: Some([counter; 32]), + })) + } +} +impl SourceContext for T {} diff --git a/crates/core/component/sct/src/component/tree.rs b/crates/core/component/sct/src/component/tree.rs new file mode 100644 index 0000000000..897226d619 --- /dev/null +++ b/crates/core/component/sct/src/component/tree.rs @@ -0,0 +1,241 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use penumbra_tct as tct; +use tct::builder::{block, epoch}; +use tracing::instrument; + +use crate::{ + component::clock::EpochRead, event, state_key, CommitmentSource, NullificationInfo, Nullifier, +}; + +#[async_trait] +/// Provides read access to the state commitment tree and related data. +pub trait SctRead: StateRead { + /// Fetch the state commitment tree from nonverifiable storage, preferring the cached tree if + /// it exists. + async fn get_sct(&self) -> tct::Tree { + // If we have a cached tree, use that. + if let Some(tree) = self.object_get(state_key::cache::cached_state_commitment_tree()) { + return tree; + } + + match self + .nonverifiable_get_raw(state_key::tree::state_commitment_tree().as_bytes()) + .await + .expect("able to retrieve state commitment tree from nonverifiable storage") + { + Some(bytes) => bincode::deserialize(&bytes).expect( + "able to deserialize stored state commitment tree from nonverifiable storage", + ), + None => tct::Tree::new(), + } + } + + /// Return the SCT root for the given height, if it exists. + /// If the height is not found, return `None`. + async fn get_anchor_by_height(&self, height: u64) -> Result> { + self.get(&state_key::tree::anchor_by_height(height)).await + } + + /// Return metadata on the specified nullifier, if it has been spent. + async fn spend_info(&self, nullifier: Nullifier) -> Result> { + self.get(&state_key::nullifier_set::spent_nullifier_lookup( + &nullifier, + )) + .await + } + + /// Return the set of nullifiers that have been spent in the current block. + fn pending_nullifiers(&self) -> im::Vector { + self.object_get(state_key::nullifier_set::pending_nullifiers()) + .unwrap_or_default() + } +} + +impl SctRead for T {} + +#[async_trait] +/// Provides write access to the state commitment tree and related data. +pub trait SctManager: StateWrite { + /// Write an SCT instance to nonverifiable storage and record + /// the block and epoch roots in the JMT. + /// + /// # Panics + /// If the epoch has not been set, or if a serialization failure occurs. + async fn write_sct( + &mut self, + height: u64, + sct: tct::Tree, + block_root: block::Root, + epoch_root: Option, + ) { + let sct_anchor = sct.root(); + + // Write the anchor as a key, so we can check claimed anchors... + self.put_proto(state_key::tree::anchor_lookup(sct_anchor), height); + // ... and as a value, so we can check SCT consistency. + // TODO: can we move this out to NV storage? + self.put(state_key::tree::anchor_by_height(height), sct_anchor); + + self.record_proto(event::anchor(height, sct_anchor)); + self.record_proto(event::block_root(height, block_root)); + // Only record an epoch root event if we are ending the epoch. + if let Some(epoch_root) = epoch_root { + let index = self + .get_current_epoch() + .await + .expect("epoch must be set") + .index; + self.record_proto(event::epoch_root(index, epoch_root)); + } + + self.write_sct_cache(sct); + self.persist_sct_cache(); + } + + /// Add a state commitment into the SCT, emitting an event recording its + /// source, and return the insert position in the tree. + async fn add_sct_commitment( + &mut self, + commitment: tct::StateCommitment, + source: CommitmentSource, + ) -> Result { + // Record in the SCT + let mut tree = self.get_sct().await; + let position = tree.insert(tct::Witness::Forget, commitment)?; + self.write_sct_cache(tree); + + // Record the commitment source in an event + self.record_proto(event::commitment(commitment, position, source)); + + Ok(position) + } + + #[instrument(skip(self, source))] + /// Record a nullifier as spent in the verifiable storage. + async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) { + tracing::debug!("marking as spent"); + + // We need to record the nullifier as spent in the JMT (to prevent + // double spends), as well as in the CompactBlock (so that clients + // can learn that their note was spent). + self.put( + state_key::nullifier_set::spent_nullifier_lookup(&nullifier), + // We don't use the value for validity checks, but writing the source + // here lets us find out what transaction spent the nullifier. + NullificationInfo { + id: source + .id() + .expect("nullifiers are only consumed by transactions"), + spend_height: self.get_block_height().await.expect("block height is set"), + }, + ); + + // Record the nullifier to be inserted into the compact block + let mut nullifiers = self.pending_nullifiers(); + nullifiers.push_back(nullifier); + self.object_put(state_key::nullifier_set::pending_nullifiers(), nullifiers); + } + + /// Seal the current block in the SCT, and produce an epoch root if + /// we are ending an epoch as well. + /// + /// # Panics + /// This method panic if the block is full, or if a serialization failure occurs. + async fn end_sct_block( + &mut self, + end_epoch: bool, + ) -> Result<(block::Root, Option)> { + let height = self.get_block_height().await?; + + let mut tree = self.get_sct().await; + + // Close the block in the SCT + let block_root = tree + .end_block() + .expect("ending a block in the state commitment tree can never fail"); + + // If the block ends an epoch, also close the epoch in the SCT + let epoch_root = if end_epoch { + let epoch_root = tree + .end_epoch() + .expect("ending an epoch in the state commitment tree can never fail"); + Some(epoch_root) + } else { + None + }; + + self.write_sct(height, tree, block_root, epoch_root).await; + + Ok((block_root, epoch_root)) + } + + // Set the state commitment tree in memory, but without committing to it in the nonverifiable + // storage (very cheap). + fn write_sct_cache(&mut self, tree: tct::Tree) { + self.object_put(state_key::cache::cached_state_commitment_tree(), tree); + } + + /// Persist the object-store SCT instance to nonverifiable storage. + /// Note that this doesn't actually persist the SCT to disk, see the + /// cndiarium documentation for more information. + /// + /// # Panics + /// This method panics if a serialization failure occurs. + fn persist_sct_cache(&mut self) { + // If the cached tree is dirty, flush it to storage + if let Some(tree) = + self.object_get::(state_key::cache::cached_state_commitment_tree()) + { + let bytes = bincode::serialize(&tree) + .expect("able to serialize state commitment tree to bincode"); + self.nonverifiable_put_raw( + state_key::tree::state_commitment_tree().as_bytes().to_vec(), + bytes, + ); + } + } +} + +impl SctManager for T {} + +#[async_trait] +pub trait VerificationExt: StateRead { + async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> { + if anchor.is_empty() { + return Ok(()); + } + + if let Some(anchor_height) = self + .get_proto::(&state_key::tree::anchor_lookup(anchor)) + .await? + { + tracing::debug!(?anchor, ?anchor_height, "anchor is valid"); + Ok(()) + } else { + Err(anyhow!( + "provided anchor {} is not a valid SCT root", + anchor + )) + } + } + + async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> { + if let Some(info) = self + .get::(&state_key::nullifier_set::spent_nullifier_lookup( + &nullifier, + )) + .await? + { + anyhow::bail!( + "nullifier {} was already spent in {:?}", + nullifier, + hex::encode(info.id), + ); + } + Ok(()) + } +} +impl VerificationExt for T {} diff --git a/crates/core/component/sct/src/component/view.rs b/crates/core/component/sct/src/component/view.rs deleted file mode 100644 index 88c7e7eed9..0000000000 --- a/crates/core/component/sct/src/component/view.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::str::FromStr; - -use anyhow::{anyhow, Context, Result}; -use async_trait::async_trait; -use cnidarium::{StateRead, StateWrite}; -use penumbra_proto::{StateReadProto, StateWriteProto}; -use penumbra_tct as tct; -use tct::builder::{block, epoch}; -use tracing::instrument; - -use crate::{ - epoch::Epoch, event, params::SctParameters, state_key, CommitmentSource, NullificationInfo, - Nullifier, -}; - -/// A helper trait for placing a `CommitmentSource` as ambient context during execution. -#[async_trait] -pub trait SourceContext: StateWrite { - fn put_current_source(&mut self, source: Option) { - if let Some(source) = source { - self.object_put(state_key::current_source(), source) - } else { - self.object_delete(state_key::current_source()) - } - } - - fn get_current_source(&self) -> Option { - self.object_get(state_key::current_source()) - } - - /// Sets a mock source, for testing. - /// - /// The `counter` field allows distinguishing hashes at different stages of the test. - fn put_mock_source(&mut self, counter: u8) { - self.put_current_source(Some(CommitmentSource::Transaction { - id: Some([counter; 32]), - })) - } -} - -impl SourceContext for T {} - -#[async_trait] -/// Provides read access to the block eights, epoch, and other related data. -pub trait EpochRead: StateRead { - /// Get the current block height. - async fn get_block_height(&self) -> Result { - self.get_proto(state_key::block_manager::block_height()) - .await? - .ok_or_else(|| anyhow!("Missing block_height")) - } - - /// Gets the current block timestamp from the JMT - async fn get_block_timestamp(&self) -> Result { - let timestamp_string: String = self - .get_proto(state_key::block_manager::block_timestamp()) - .await? - .ok_or_else(|| anyhow!("Missing block_timestamp"))?; - - Ok(tendermint::Time::from_str(×tamp_string) - .context("block_timestamp was an invalid RFC3339 time string")?) - } - - /// Get the current epoch. - async fn current_epoch(&self) -> Result { - // Get the height - let height = self.get_block_height().await?; - - self.get(&state_key::epoch_manager::epoch_by_height(height)) - .await? - .ok_or_else(|| anyhow!("missing epoch for current height: {height}")) - } - - async fn epoch_by_height(&self, height: u64) -> Result { - self.get(&state_key::epoch_manager::epoch_by_height(height)) - .await? - .ok_or_else(|| anyhow!("missing epoch for height")) - } - - // Returns true if the epoch is ending early this block. - fn epoch_ending_early(&self) -> bool { - self.object_get(state_key::epoch_manager::end_epoch_early()) - .unwrap_or(false) - } - - /// Gets the epoch duration for the chain (in blocks). - async fn get_epoch_duration(&self) -> Result { - self.get_sct_params() - .await - .map(|params| params.epoch_duration) - } -} - -impl EpochRead for T {} - -/// This trait provides read access to common parts of the Penumbra -/// state store. -/// -/// Note: the `get_` methods in this trait assume that the state store has been -/// initialized, so they will error on an empty state. -#[async_trait] -pub trait StateReadExt: StateRead { - /// Gets the fee parameters from the JMT. - async fn get_sct_params(&self) -> Result { - self.get(state_key::sct_params()) - .await? - .ok_or_else(|| anyhow!("Missing SctParameters")) - } - /// Indicates if the sct parameters have been updated in this block. - fn sct_params_updated(&self) -> bool { - self.object_get::<()>(state_key::sct_params_updated()) - .is_some() - } - - async fn state_commitment_tree(&self) -> tct::Tree { - // If we have a cached tree, use that. - if let Some(tree) = self.object_get(state_key::cached_state_commitment_tree()) { - return tree; - } - - match self - .nonverifiable_get_raw(state_key::state_commitment_tree().as_bytes()) - .await - .expect("able to retrieve state commitment tree from nonverifiable storage") - { - Some(bytes) => bincode::deserialize(&bytes).expect( - "able to deserialize stored state commitment tree from nonverifiable storage", - ), - None => tct::Tree::new(), - } - } - - async fn anchor_by_height(&self, height: u64) -> Result> { - self.get(&state_key::anchor_by_height(height)).await - } - - async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> { - if anchor.is_empty() { - return Ok(()); - } - - if let Some(anchor_height) = self - .get_proto::(&state_key::anchor_lookup(anchor)) - .await? - { - tracing::debug!(?anchor, ?anchor_height, "anchor is valid"); - Ok(()) - } else { - Err(anyhow!( - "provided anchor {} is not a valid SCT root", - anchor - )) - } - } - - async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> { - if let Some(info) = self - .get::(&state_key::spent_nullifier_lookup(&nullifier)) - .await? - { - anyhow::bail!( - "nullifier {} was already spent in {:?}", - nullifier, - hex::encode(&info.id), - ); - } - Ok(()) - } - - async fn spend_info(&self, nullifier: Nullifier) -> Result> { - self.get(&state_key::spent_nullifier_lookup(&nullifier)) - .await - } - - fn pending_nullifiers(&self) -> im::Vector { - self.object_get(state_key::pending_nullifiers()) - .unwrap_or_default() - } -} - -impl StateReadExt for T {} - -#[async_trait] -pub trait SctManager: StateWrite { - #[instrument(skip(self, source))] - async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) { - tracing::debug!("marking as spent"); - - // We need to record the nullifier as spent in the JMT (to prevent - // double spends), as well as in the CompactBlock (so that clients - // can learn that their note was spent). - self.put( - state_key::spent_nullifier_lookup(&nullifier), - // We don't use the value for validity checks, but writing the source - // here lets us find out what transaction spent the nullifier. - NullificationInfo { - id: source - .id() - .expect("nullifiers are only consumed by transactions"), - spend_height: self.get_block_height().await.expect("block height is set"), - }, - ); - - // Record the nullifier to be inserted into the compact block - let mut nullifiers = self.pending_nullifiers(); - nullifiers.push_back(nullifier); - self.object_put(state_key::pending_nullifiers(), nullifiers); - } - - async fn add_sct_commitment( - &mut self, - commitment: tct::StateCommitment, - source: CommitmentSource, - ) -> Result { - // Record in the SCT - let mut tree = self.state_commitment_tree().await; - let position = tree.insert(tct::Witness::Forget, commitment)?; - self.put_state_commitment_tree(tree); - - // Record the commitment source in an event - self.record_proto(event::commitment(commitment, position, source)); - - Ok(position) - } - - async fn end_sct_block( - &mut self, - end_epoch: bool, - ) -> Result<(block::Root, Option)> { - let height = self.get_block_height().await?; - - let mut tree = self.state_commitment_tree().await; - - // Close the block in the SCT - let block_root = tree - .end_block() - .expect("ending a block in the state commitment tree can never fail"); - - // If the block ends an epoch, also close the epoch in the SCT - let epoch_root = if end_epoch { - let epoch_root = tree - .end_epoch() - .expect("ending an epoch in the state commitment tree can never fail"); - Some(epoch_root) - } else { - None - }; - - self.write_sct(height, tree, block_root, epoch_root).await; - - Ok((block_root, epoch_root)) - } -} - -impl SctManager for T {} - -/// This trait provides write access to common parts of the Penumbra -/// state store. -/// -/// Note: the `get_` methods in this trait assume that the state store has been -/// initialized, so they will error on an empty state. -//#[async_trait(?Send)] -#[async_trait] -trait StateWriteExt: StateWrite { - // Set the state commitment tree in memory, but without committing to it in the nonverifiable - // storage (very cheap). - fn put_state_commitment_tree(&mut self, tree: tct::Tree) { - self.object_put(state_key::cached_state_commitment_tree(), tree); - } - - // Serialize the current state commitment tree to storage (slightly more expensive, should only - // happen once a block). - async fn write_state_commitment_tree(&mut self) { - // If the cached tree is dirty, flush it to storage - if let Some(tree) = self.object_get::(state_key::cached_state_commitment_tree()) - { - let bytes = bincode::serialize(&tree) - .expect("able to serialize state commitment tree to bincode"); - self.nonverifiable_put_raw( - state_key::state_commitment_tree().as_bytes().to_vec(), - bytes, - ); - } - } - - async fn write_sct( - &mut self, - height: u64, - sct: tct::Tree, - block_root: block::Root, - epoch_root: Option, - ) { - let sct_anchor = sct.root(); - - // Write the anchor as a key, so we can check claimed anchors... - self.put_proto(state_key::anchor_lookup(sct_anchor), height); - // ... and as a value, so we can check SCT consistency. - // TODO: can we move this out to NV storage? - self.put(state_key::anchor_by_height(height), sct_anchor); - - self.record_proto(event::anchor(height, sct_anchor)); - self.record_proto(event::block_root(height, block_root)); - // Only record an epoch root event if we are ending the epoch. - if let Some(epoch_root) = epoch_root { - let index = self.current_epoch().await.expect("epoch must be set").index; - self.record_proto(event::epoch_root(index, epoch_root)); - } - - self.put_state_commitment_tree(sct); - self.write_state_commitment_tree().await; - } -} - -impl StateWriteExt for T {} - -#[async_trait] -pub trait EpochManager: StateWrite { - /// Writes the block timestamp to the JMT - fn put_block_timestamp(&mut self, timestamp: tendermint::Time) { - self.put_proto( - state_key::block_manager::block_timestamp().into(), - timestamp.to_rfc3339(), - ) - } - - // Signals that the epoch should end this block. - fn signal_end_epoch(&mut self) { - self.object_put(state_key::epoch_manager::end_epoch_early(), true) - } - - /// Writes the block height to the JMT - fn put_block_height(&mut self, height: u64) { - self.put_proto(state_key::block_manager::block_height().to_string(), height) - } - - /// Writes the epoch for the current height - fn put_epoch_by_height(&mut self, height: u64, epoch: Epoch) { - self.put(state_key::epoch_manager::epoch_by_height(height), epoch) - } -} - -impl EpochManager for T {} - -#[async_trait] -// MERGEBLOCK(erwan): rename trait -pub trait SctParameterWriter: StateWrite { - /// Writes the SCT parameters to the JMT - fn put_sct_params(&mut self, params: SctParameters) { - self.put(state_key::sct_params().to_string(), params); - self.object_put(state_key::sct_params_updated(), ()) - } -} -impl SctParameterWriter for T {} diff --git a/crates/core/component/sct/src/state_key.rs b/crates/core/component/sct/src/state_key.rs index 149ca92ac2..0d34dbf263 100644 --- a/crates/core/component/sct/src/state_key.rs +++ b/crates/core/component/sct/src/state_key.rs @@ -1,15 +1,11 @@ -use std::string::String; - -use penumbra_tct::{Root, StateCommitment}; - -use crate::Nullifier; - -pub fn sct_params() -> &'static str { - "sct/params" -} +pub mod config { + pub fn sct_params() -> &'static str { + "sct/config/sct_params" + } -pub fn sct_params_updated() -> &'static str { - "sct/sct_params_updated" + pub fn sct_params_updated() -> &'static str { + "sct/config/sct_params_updated" + } } pub mod block_manager { @@ -36,35 +32,44 @@ pub mod epoch_manager { } } -pub fn spent_nullifier_lookup(nullifier: &Nullifier) -> String { - format!("sct/nf/{nullifier}") -} +pub mod nullifier_set { + use crate::Nullifier; -pub fn pending_nullifiers() -> &'static str { - "sct/pending_nullifiers" -} + pub fn spent_nullifier_lookup(nullifier: &Nullifier) -> String { + format!("sct/nullifier_set/spent_nullifier_lookup/{}", nullifier) + } -pub fn anchor_by_height(height: u64) -> String { - format!("sct/anchor/{height}") + pub fn pending_nullifiers() -> &'static str { + "sct/nullifier_set/pending_nullifiers" + } } -pub fn anchor_lookup(anchor: Root) -> String { - format!("sct/valid_anchors/{anchor}") -} +pub mod tree { + pub fn anchor_by_height(height: u64) -> String { + format!("sct/tree/anchor_by_height/{}", height) + } -pub fn state_commitment_tree() -> &'static str { - "sct/state_commitment_tree" -} + pub fn anchor_lookup(anchor: penumbra_tct::Root) -> String { + format!("sct/tree/anchor_lookup/{}", anchor) + } -pub fn note_source(note_commitment: &StateCommitment) -> String { - format!("sct/note_source/{note_commitment}") + pub fn state_commitment_tree() -> &'static str { + "sct/tree/state_commitment_tree" + } + + pub fn note_source(note_commitment: &penumbra_tct::StateCommitment) -> String { + format!("sct/tree/note_source/{}", note_commitment) + } } -// In-memory state key for caching the current SCT (avoids serialization overhead) -pub fn cached_state_commitment_tree() -> &'static str { - "sct/cached_state_commitment_tree" +pub mod cache { + pub fn cached_state_commitment_tree() -> &'static str { + "sct/cache/cached_state_commitment_tree" + } } -pub fn current_source() -> &'static str { - "sct/current_source" +pub mod ambient { + pub fn current_source() -> &'static str { + "sct/ambient/current_source" + } } diff --git a/crates/core/component/shielded-pool/src/component/action_handler/output.rs b/crates/core/component/shielded-pool/src/component/action_handler/output.rs index a0fc312da6..81559b20e2 100644 --- a/crates/core/component/shielded-pool/src/component/action_handler/output.rs +++ b/crates/core/component/shielded-pool/src/component/action_handler/output.rs @@ -6,7 +6,7 @@ use cnidarium::{StateRead, StateWrite}; use cnidarium_component::ActionHandler; use penumbra_proof_params::OUTPUT_PROOF_VERIFICATION_KEY; use penumbra_proto::StateWriteProto as _; -use penumbra_sct::component::SourceContext; +use penumbra_sct::component::source::SourceContext; use crate::{component::NoteManager, event, output::OutputProofPublic, Output}; diff --git a/crates/core/component/shielded-pool/src/component/action_handler/spend.rs b/crates/core/component/shielded-pool/src/component/action_handler/spend.rs index a75375c54c..dd0419f487 100644 --- a/crates/core/component/shielded-pool/src/component/action_handler/spend.rs +++ b/crates/core/component/shielded-pool/src/component/action_handler/spend.rs @@ -6,7 +6,10 @@ use cnidarium::{StateRead, StateWrite}; use cnidarium_component::ActionHandler; use penumbra_proof_params::SPEND_PROOF_VERIFICATION_KEY; use penumbra_proto::StateWriteProto as _; -use penumbra_sct::component::{SctManager, SourceContext, StateReadExt as _}; +use penumbra_sct::component::{ + source::SourceContext, + tree::{SctManager, VerificationExt}, +}; use penumbra_txhash::TransactionContext; use crate::{event, Spend, SpendProofPublic}; diff --git a/crates/core/component/shielded-pool/src/component/note_manager.rs b/crates/core/component/shielded-pool/src/component/note_manager.rs index 1a34d28f6f..c2e61c1c7f 100644 --- a/crates/core/component/shielded-pool/src/component/note_manager.rs +++ b/crates/core/component/shielded-pool/src/component/note_manager.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use cnidarium::StateWrite; use penumbra_asset::Value; use penumbra_keys::Address; -use penumbra_sct::component::{SctManager as _, StateReadExt as _}; +use penumbra_sct::component::tree::{SctManager, SctRead}; use penumbra_sct::CommitmentSource; use penumbra_tct as tct; use tct::StateCommitment; @@ -41,7 +41,7 @@ pub trait NoteManager: StateWrite { // is very slow, so instead we hash the current position. let position: u64 = self - .state_commitment_tree() + .get_sct() .await .position() .expect("state commitment tree is not full") 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 92700a92ad..ca4e52efff 100644 --- a/crates/core/component/shielded-pool/src/component/shielded_pool.rs +++ b/crates/core/component/shielded-pool/src/component/shielded_pool.rs @@ -26,7 +26,7 @@ impl Component for ShieldedPool { match app_state { None => { /* Checkpoint -- no-op */ } Some(genesis) => { - // MERGEBLOCK(erwan): the handling of those parameters is a bit weird. + // TODO(erwan): the handling of those parameters is a bit weird. // rationalize it before merging state.put_shielded_pool_params(genesis.shielded_pool_params.clone()); state.put_current_fmd_parameters(fmd::Parameters::default()); diff --git a/crates/core/component/stake/src/action_handler/undelegate_claim.rs b/crates/core/component/stake/src/action_handler/undelegate_claim.rs index c501119b00..170c291a7f 100644 --- a/crates/core/component/stake/src/action_handler/undelegate_claim.rs +++ b/crates/core/component/stake/src/action_handler/undelegate_claim.rs @@ -4,7 +4,7 @@ use anyhow::{ensure, Result}; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use penumbra_proof_params::CONVERT_PROOF_VERIFICATION_KEY; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use crate::undelegate_claim::UndelegateClaimProofPublic; use crate::{action_handler::ActionHandler, UnbondingToken}; @@ -32,7 +32,7 @@ impl ActionHandler for UndelegateClaim { async fn check_stateful(&self, state: Arc) -> Result<()> { // If the validator delegation pool is bonded, or unbonding, check that enough epochs // have elapsed to claim the unbonding tokens: - let current_epoch = state.current_epoch().await?; + let current_epoch = state.get_current_epoch().await?; let unbonding_epoch = state .compute_unbonding_epoch_for_validator(&self.body.validator_identity) .await?; diff --git a/crates/core/component/stake/src/action_handler/validator_definition.rs b/crates/core/component/stake/src/action_handler/validator_definition.rs index b80531da26..928b01653d 100644 --- a/crates/core/component/stake/src/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/action_handler/validator_definition.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; -use penumbra_sct::component::EpochRead; +use penumbra_sct::component::clock::EpochRead; use std::sync::Arc; @@ -103,7 +103,7 @@ impl ActionHandler for validator::Definition { let v = self; let current_epoch = state - .current_epoch() + .get_current_epoch() .await .context("should be able to get current epoch during validator definition execution")?; diff --git a/crates/core/component/stake/src/component.rs b/crates/core/component/stake/src/component.rs index d112288b63..1d5a8aa5f0 100644 --- a/crates/core/component/stake/src/component.rs +++ b/crates/core/component/stake/src/component.rs @@ -1,7 +1,7 @@ // Implementation of a pd component for the staking system. use penumbra_distributions::component::StateReadExt as _; use penumbra_sct::{ - component::{EpochManager, EpochRead}, + component::clock::{EpochManager, EpochRead}, epoch::Epoch, }; use std::{ @@ -165,7 +165,7 @@ pub(crate) trait StakingImpl: StateWriteExt { // triggered by epoch transitions themselves, or don't immediately affect the active // validator set. if let (Active, Defined | Disabled | Jailed | Tombstoned) = (old_state, new_state) { - self.signal_end_epoch(); + self.set_end_epoch_flag(); } match (old_state, new_state) { @@ -723,7 +723,7 @@ pub(crate) trait StakingImpl: StateWriteExt { /// unbonding target has been reached. #[instrument(skip(self))] async fn process_validator_unbondings(&mut self) -> Result<()> { - let current_epoch = self.current_epoch().await?; + let current_epoch = self.get_current_epoch().await?; let mut validator_identity_stream = self.consensus_set_stream()?; while let Some(identity_key) = validator_identity_stream.next().await { @@ -1133,7 +1133,7 @@ impl Component for Staking { .await .expect("should be able to get initial block height"); let starting_epoch = state - .epoch_by_height(starting_height) + .get_epoch_by_height(starting_height) .await .expect("should be able to get initial epoch"); let epoch_index = starting_epoch.index; @@ -1236,7 +1236,7 @@ impl Component for Staking { async fn end_epoch(state: &mut Arc) -> anyhow::Result<()> { let state = Arc::get_mut(state).context("state should be unique")?; let epoch_ending = state - .current_epoch() + .get_current_epoch() .await .context("should be able to get current epoch during end_epoch")?; state.end_epoch(epoch_ending).await?; @@ -1514,7 +1514,7 @@ pub trait StateReadExt: StateRead { /// This is the minimum of the default unbonding epoch and the validator's /// unbonding epoch. async fn compute_unbonding_epoch_for_validator(&self, id: &IdentityKey) -> Result { - let current_epoch = self.current_epoch().await?; + let current_epoch = self.get_current_epoch().await?; let unbonding_delay = self .compute_unbonding_delay_for_validator(current_epoch, id) .await?; @@ -1715,7 +1715,7 @@ pub trait StateWriteExt: StateWrite { identity_key: &IdentityKey, slashing_penalty: Penalty, ) -> Result<()> { - let current_epoch_index = self.current_epoch().await?.index; + let current_epoch_index = self.get_current_epoch().await?.index; let current_penalty = self .penalty_in_epoch(identity_key, current_epoch_index) diff --git a/crates/view/src/worker.rs b/crates/view/src/worker.rs index c86eafb798..a4928ee408 100644 --- a/crates/view/src/worker.rs +++ b/crates/view/src/worker.rs @@ -451,7 +451,7 @@ async fn sct_divergence_check( let value = client .key_value(penumbra_proto::cnidarium::v1alpha1::KeyValueRequest { - key: sct_state_key::anchor_by_height(height), + key: sct_state_key::tree::anchor_by_height(height), ..Default::default() }) .await?