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?