diff --git a/Cargo.lock b/Cargo.lock index efa355b140..99622f5da1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5814,6 +5814,7 @@ dependencies = [ "penumbra-app", "penumbra-asset", "penumbra-dex", + "penumbra-governance", "penumbra-num", "penumbra-proto", "penumbra-shielded-pool", diff --git a/crates/bin/pindexer/Cargo.toml b/crates/bin/pindexer/Cargo.toml index 5d2c82c7ec..86805da998 100644 --- a/crates/bin/pindexer/Cargo.toml +++ b/crates/bin/pindexer/Cargo.toml @@ -19,10 +19,11 @@ penumbra-shielded-pool = {workspace = true, default-features = false} penumbra-stake = {workspace = true, default-features = false} penumbra-app = {workspace = true, default-features = false} penumbra-dex = {workspace = true, default-features = false} +penumbra-governance = {workspace = true, default-features = false} penumbra-num = {workspace = true, default-features = false} penumbra-asset = {workspace = true, default-features = false} penumbra-proto = {workspace = true, default-features = false} +tracing = {workspace = true} tokio = {workspace = true, features = ["full"]} serde_json = {workspace = true} sqlx = { workspace = true, features = ["chrono", "postgres"] } -tracing = {workspace = true} diff --git a/crates/bin/pindexer/src/governance.rs b/crates/bin/pindexer/src/governance.rs new file mode 100644 index 0000000000..4d2a1a91fc --- /dev/null +++ b/crates/bin/pindexer/src/governance.rs @@ -0,0 +1,550 @@ +use anyhow::{anyhow, Context, Result}; +use cometindex::{async_trait, sqlx, AppView, ContextualizedEvent, PgTransaction}; +use penumbra_governance::{ + proposal::ProposalPayloadToml, proposal_state, DelegatorVote, Proposal, ProposalDepositClaim, + ProposalWithdraw, ValidatorVote, +}; +use penumbra_num::Amount; +use penumbra_proto::{ + core::component::{ + governance::v1::{self as pb}, + sct::v1 as sct_pb, + }, + event::ProtoEvent, +}; +use penumbra_stake::IdentityKey; +use sqlx::PgPool; + +#[derive(Debug)] +pub struct GovernanceProposals {} + +const EVENT_PROPOSAL_SUBMIT: &str = "penumbra.core.component.governance.v1.EventProposalSubmit"; +const EVENT_DELEGATOR_VOTE: &str = "penumbra.core.component.governance.v1.EventDelegatorVote"; +const EVENT_VALIDATOR_VOTE: &str = "penumbra.core.component.governance.v1.EventValidatorVote"; +const EVENT_PROPOSAL_WITHDRAW: &str = "penumbra.core.component.governance.v1.EventProposalWithdraw"; +const EVENT_PROPOSAL_PASSED: &str = "penumbra.core.component.governance.v1.EventProposalPassed"; +const EVENT_PROPOSAL_FAILED: &str = "penumbra.core.component.governance.v1.EventProposalFailed"; +const EVENT_PROPOSAL_SLASHED: &str = "penumbra.core.component.governance.v1.EventProposalSlashed"; +const EVENT_PROPOSAL_DEPOSIT_CLAIM: &str = + "penumbra.core.component.governance.v1.EventProposalDepositClaim"; +const EVENT_BLOCK_ROOT: &str = "penumbra.core.component.sct.v1.EventBlockRoot"; +const ALL_RELEVANT_EVENTS: &[&str] = &[ + EVENT_PROPOSAL_SUBMIT, + EVENT_DELEGATOR_VOTE, + EVENT_VALIDATOR_VOTE, + EVENT_PROPOSAL_WITHDRAW, + EVENT_PROPOSAL_PASSED, + EVENT_PROPOSAL_FAILED, + EVENT_PROPOSAL_SLASHED, + EVENT_PROPOSAL_DEPOSIT_CLAIM, + EVENT_BLOCK_ROOT, +]; + +#[async_trait] +impl AppView for GovernanceProposals { + async fn init_chain( + &self, + dbtx: &mut PgTransaction, + _app_state: &serde_json::Value, + ) -> Result<(), anyhow::Error> { + // Define table structures + let tables = vec![ + ( + "governance_proposals", + r" + id SERIAL PRIMARY KEY, + proposal_id INTEGER NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL, + kind JSONB NOT NULL, + payload JSONB, + start_block_height BIGINT NOT NULL, + end_block_height BIGINT NOT NULL, + state JSONB NOT NULL, + proposal_deposit_amount BIGINT NOT NULL, + withdrawn BOOLEAN DEFAULT FALSE, + withdrawal_reason TEXT + ", + ), + ( + "governance_validator_votes", + r" + id SERIAL PRIMARY KEY, + proposal_id INTEGER NOT NULL, + identity_key TEXT NOT NULL, + vote JSONB NOT NULL, + voting_power BIGINT NOT NULL, + block_height BIGINT NOT NULL, + FOREIGN KEY (proposal_id) REFERENCES governance_proposals(proposal_id) + ", + ), + ( + "governance_delegator_votes", + r" + id SERIAL PRIMARY KEY, + proposal_id INTEGER NOT NULL, + identity_key TEXT NOT NULL, + vote JSONB NOT NULL, + voting_power BIGINT NOT NULL, + block_height BIGINT NOT NULL, + FOREIGN KEY (proposal_id) REFERENCES governance_proposals(proposal_id) + ", + ), + ]; + + // Define indexes + let indexes = vec![ + ( + "governance_proposals", + "proposal_id", + "idx_governance_proposals_id", + ), + ( + "governance_proposals", + "title text_pattern_ops", + "idx_governance_proposals_title", + ), + ( + "governance_proposals", + "kind", + "idx_governance_proposals_kind", + ), + ( + "governance_proposals", + "start_block_height DESC", + "idx_governance_proposals_start_block_height", + ), + ( + "governance_proposals", + "end_block_height DESC", + "idx_governance_proposals_end_block_height", + ), + ( + "governance_proposals", + "state", + "idx_governance_proposals_state", + ), + ( + "governance_proposals", + "withdrawn", + "idx_governance_proposals_withdrawn", + ), + ( + "governance_validator_votes", + "proposal_id", + "idx_governance_validator_votes_proposal_id", + ), + ( + "governance_validator_votes", + "identity_key", + "idx_governance_validator_votes_identity_key", + ), + ( + "governance_validator_votes", + "vote", + "idx_governance_validator_votes_vote", + ), + ( + "governance_validator_votes", + "voting_power", + "idx_governance_validator_votes_voting_power", + ), + ( + "governance_validator_votes", + "block_height", + "idx_governance_validator_votes_block_height", + ), + ( + "governance_delegator_votes", + "proposal_id", + "idx_governance_delegator_votes_proposal_id", + ), + ( + "governance_delegator_votes", + "identity_key", + "idx_governance_delegator_votes_identity_key", + ), + ( + "governance_delegator_votes", + "vote", + "idx_governance_delegator_votes_vote", + ), + ( + "governance_delegator_votes", + "voting_power", + "idx_governance_delegator_votes_voting_power", + ), + ( + "governance_delegator_votes", + "block_height", + "idx_governance_delegator_votes_block_height", + ), + ]; + + async fn create_table( + dbtx: &mut PgTransaction<'_>, + table_name: &str, + structure: &str, + ) -> Result<()> { + let query = format!("CREATE TABLE IF NOT EXISTS {} ({})", table_name, structure); + sqlx::query(&query).execute(dbtx.as_mut()).await?; + Ok(()) + } + + async fn create_index( + dbtx: &mut PgTransaction<'_>, + table_name: &str, + column: &str, + index_name: &str, + ) -> Result<()> { + let query = format!( + "CREATE INDEX IF NOT EXISTS {} ON {}({})", + index_name, table_name, column + ); + sqlx::query(&query).execute(dbtx.as_mut()).await?; + Ok(()) + } + + // Create tables + for (table_name, table_structure) in tables { + create_table(dbtx, table_name, table_structure).await?; + } + + // Create indexes + for (table_name, column, index_name) in indexes { + create_index(dbtx, table_name, column, index_name).await?; + } + + Ok(()) + } + + fn is_relevant(&self, type_str: &str) -> bool { + ALL_RELEVANT_EVENTS.contains(&type_str) + } + + async fn index_event( + &self, + dbtx: &mut PgTransaction, + event: &ContextualizedEvent, + _src_db: &PgPool, + ) -> Result<(), anyhow::Error> { + match event.event.kind.as_str() { + EVENT_PROPOSAL_SUBMIT => { + let pe = pb::EventProposalSubmit::from_event(event.as_ref())?; + let start_block_height = pe.start_height; + let end_block_height = pe.end_height; + let submit = pe + .submit + .ok_or_else(|| anyhow!("missing submit in event"))?; + let deposit_amount = submit + .deposit_amount + .ok_or_else(|| anyhow!("missing deposit amount in event"))? + .try_into() + .context("error converting deposit amount")?; + let proposal = submit + .proposal + .ok_or_else(|| anyhow!("missing proposal in event"))? + .try_into() + .context("error converting proposal")?; + handle_proposal_submit( + dbtx, + proposal, + deposit_amount, + start_block_height, + end_block_height, + event.block_height, + ) + .await?; + } + EVENT_DELEGATOR_VOTE => { + let pe = pb::EventDelegatorVote::from_event(event.as_ref())?; + let vote = pe + .vote + .ok_or_else(|| anyhow!("missing vote in event"))? + .try_into() + .context("error converting delegator vote")?; + let validator_identity_key = pe + .validator_identity_key + .ok_or_else(|| anyhow!("missing validator identity key in event"))? + .try_into() + .context("error converting validator identity key")?; + handle_delegator_vote(dbtx, vote, validator_identity_key, event.block_height) + .await?; + } + EVENT_VALIDATOR_VOTE => { + let pe = pb::EventValidatorVote::from_event(event.as_ref())?; + let voting_power = pe.voting_power; + let vote = pe + .vote + .ok_or_else(|| anyhow!("missing vote in event"))? + .try_into() + .context("error converting vote")?; + handle_validator_vote(dbtx, vote, voting_power, event.block_height).await?; + } + EVENT_PROPOSAL_WITHDRAW => { + let pe = pb::EventProposalWithdraw::from_event(event.as_ref())?; + let proposal_withdraw: ProposalWithdraw = pe + .withdraw + .ok_or_else(|| anyhow!("missing withdraw in event"))? + .try_into() + .context("error converting proposal withdraw")?; + let proposal = proposal_withdraw.proposal; + let reason = proposal_withdraw.reason; + handle_proposal_withdraw(dbtx, proposal, reason).await?; + } + EVENT_PROPOSAL_PASSED => { + let pe = pb::EventProposalPassed::from_event(event.as_ref())?; + let proposal = pe + .proposal + .ok_or_else(|| anyhow!("missing proposal in event"))? + .try_into() + .context("error converting proposal")?; + handle_proposal_passed(dbtx, proposal).await?; + } + EVENT_PROPOSAL_FAILED => { + let pe = pb::EventProposalFailed::from_event(event.as_ref())?; + let proposal = pe + .proposal + .ok_or_else(|| anyhow!("missing proposal in event"))? + .try_into() + .context("error converting proposal")?; + handle_proposal_failed(dbtx, proposal).await?; + } + EVENT_PROPOSAL_SLASHED => { + let pe = pb::EventProposalSlashed::from_event(event.as_ref())?; + let proposal = pe + .proposal + .ok_or_else(|| anyhow!("missing proposal in event"))? + .try_into() + .context("error converting proposal")?; + handle_proposal_slashed(dbtx, proposal).await?; + } + EVENT_PROPOSAL_DEPOSIT_CLAIM => { + let pe = pb::EventProposalDepositClaim::from_event(event.as_ref())?; + let deposit_claim = pe + .deposit_claim + .ok_or_else(|| anyhow!("missing deposit claim in event"))? + .try_into() + .context("error converting deposit claim")?; + handle_proposal_deposit_claim(dbtx, deposit_claim).await?; + } + EVENT_BLOCK_ROOT => { + let pe = sct_pb::EventBlockRoot::from_event(event.as_ref())?; + handle_block_root(dbtx, pe.height).await?; + } + _ => {} + } + + Ok(()) + } +} + +async fn handle_proposal_submit( + dbtx: &mut PgTransaction<'_>, + proposal: Proposal, + deposit_amount: Amount, + start_block_height: u64, + end_block_height: u64, + _block_height: u64, +) -> Result<()> { + sqlx::query( + "INSERT INTO governance_proposals ( + proposal_id, title, description, kind, payload, start_block_height, end_block_height, state, proposal_deposit_amount + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (proposal_id) DO NOTHING", + ) + .bind(proposal.id as i64) + .bind(&proposal.title) + .bind(&proposal.description) + .bind(serde_json::to_value(proposal.kind())?) + .bind(serde_json::to_value(ProposalPayloadToml::from(proposal.payload))?) + .bind(start_block_height as i64) + .bind(end_block_height as i64) + .bind(serde_json::to_value(proposal_state::State::Voting)?) + .bind(deposit_amount.value() as i64) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_delegator_vote( + dbtx: &mut PgTransaction<'_>, + vote: DelegatorVote, + identity_key: IdentityKey, + block_height: u64, +) -> Result<()> { + sqlx::query( + "INSERT INTO governance_delegator_votes ( + proposal_id, identity_key, vote, voting_power, block_height + ) + VALUES ($1, $2, $3, $4, $5)", + ) + .bind(vote.body.proposal as i64) + .bind(&identity_key.to_string()) + .bind(serde_json::to_value(vote.body.vote)?) + .bind(vote.body.unbonded_amount.value() as i64) + .bind(block_height as i64) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_validator_vote( + dbtx: &mut PgTransaction<'_>, + vote: ValidatorVote, + voting_power: u64, + block_height: u64, +) -> Result<()> { + sqlx::query( + "INSERT INTO governance_validator_votes ( + proposal_id, identity_key, vote, voting_power, block_height + ) + VALUES ($1, $2, $3, $4, $5)", + ) + .bind(vote.body.proposal as i64) + .bind(&vote.body.identity_key.to_string()) + .bind(serde_json::to_value(vote.body.vote)?) + .bind(voting_power as i64) + .bind(block_height as i64) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_proposal_withdraw( + dbtx: &mut PgTransaction<'_>, + proposal_id: u64, + reason: String, +) -> Result<()> { + sqlx::query( + "UPDATE governance_proposals + SET withdrawn = TRUE, withdrawal_reason = $2 + WHERE proposal_id = $1", + ) + .bind(proposal_id as i64) + .bind(&reason) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_proposal_passed(dbtx: &mut PgTransaction<'_>, proposal: Proposal) -> Result<()> { + sqlx::query( + "UPDATE governance_proposals + SET state = $2 + WHERE proposal_id = $1", + ) + .bind(proposal.id as i64) + .bind(serde_json::to_value(proposal_state::State::Finished { + outcome: proposal_state::Outcome::Passed, + })?) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_proposal_failed(dbtx: &mut PgTransaction<'_>, proposal: Proposal) -> Result<()> { + // Determine if the proposal was withdrawn before it concluded, and if so, why + let reason: Option = sqlx::query_scalar( + "SELECT withdrawal_reason + FROM governance_proposals + WHERE proposal_id = $1 AND withdrawn = TRUE + LIMIT 1", + ) + .bind(proposal.id as i64) + .fetch_optional(dbtx.as_mut()) + .await?; + let withdrawn = proposal_state::Withdrawn::from(reason); + + sqlx::query( + "UPDATE governance_proposals + SET state = $2 + WHERE proposal_id = $1", + ) + .bind(proposal.id as i64) + .bind(serde_json::to_value(proposal_state::State::Finished { + outcome: proposal_state::Outcome::Failed { withdrawn }, + })?) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_proposal_slashed(dbtx: &mut PgTransaction<'_>, proposal: Proposal) -> Result<()> { + // Determine if the proposal was withdrawn before it concluded, and if so, why + let reason: Option = sqlx::query_scalar( + "SELECT withdrawal_reason + FROM governance_proposals + WHERE proposal_id = $1 AND withdrawn = TRUE + LIMIT 1", + ) + .bind(proposal.id as i64) + .fetch_optional(dbtx.as_mut()) + .await?; + let withdrawn = proposal_state::Withdrawn::from(reason); + + sqlx::query( + "UPDATE governance_proposals + SET state = $2 + WHERE proposal_id = $1", + ) + .bind(proposal.id as i64) + .bind(serde_json::to_value(proposal_state::State::Finished { + outcome: proposal_state::Outcome::Slashed { withdrawn }, + })?) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_proposal_deposit_claim( + dbtx: &mut PgTransaction<'_>, + deposit_claim: ProposalDepositClaim, +) -> Result<()> { + let current_state: serde_json::Value = sqlx::query_scalar( + "SELECT state + FROM governance_proposals + WHERE proposal_id = $1", + ) + .bind(deposit_claim.proposal as i64) + .fetch_one(dbtx.as_mut()) + .await?; + + let current_state: proposal_state::State = serde_json::from_value(current_state)?; + + let outcome = match current_state { + proposal_state::State::Finished { outcome } => outcome, + _ => { + return Err(anyhow!( + "proposal {} is not in a finished state", + deposit_claim.proposal + )) + } + }; + + sqlx::query( + "UPDATE governance_proposals + SET state = $2 + WHERE proposal_id = $1", + ) + .bind(deposit_claim.proposal as i64) + .bind(serde_json::to_value(proposal_state::State::Claimed { + outcome, + })?) + .execute(dbtx.as_mut()) + .await?; + + Ok(()) +} + +async fn handle_block_root(_dbtx: &mut PgTransaction<'_>, _height: u64) -> Result<()> { + Ok(()) +} diff --git a/crates/bin/pindexer/src/indexer_ext.rs b/crates/bin/pindexer/src/indexer_ext.rs index 0bc3b0e511..80d034950d 100644 --- a/crates/bin/pindexer/src/indexer_ext.rs +++ b/crates/bin/pindexer/src/indexer_ext.rs @@ -9,6 +9,7 @@ impl IndexerExt for cometindex::Indexer { .with_index(crate::stake::Slashings {}) .with_index(crate::stake::DelegationTxs {}) .with_index(crate::stake::UndelegationTxs {}) + .with_index(crate::governance::GovernanceProposals {}) .with_index(crate::dex::Component::new()) } } diff --git a/crates/bin/pindexer/src/lib.rs b/crates/bin/pindexer/src/lib.rs index 9c443724d8..5cf2d8bd14 100644 --- a/crates/bin/pindexer/src/lib.rs +++ b/crates/bin/pindexer/src/lib.rs @@ -7,3 +7,5 @@ pub mod dex; pub mod shielded_pool; mod sql; pub mod stake; + +pub mod governance; diff --git a/crates/bin/pindexer/src/stake/delegation_txs.rs b/crates/bin/pindexer/src/stake/delegation_txs.rs index a8da474a4c..c25dc63a4d 100644 --- a/crates/bin/pindexer/src/stake/delegation_txs.rs +++ b/crates/bin/pindexer/src/stake/delegation_txs.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use cometindex::{async_trait, sqlx, AppView, ContextualizedEvent, PgPool, PgTransaction}; use penumbra_num::Amount; use penumbra_proto::{core::component::stake::v1 as pb, event::ProtoEvent}; +use penumbra_stake::IdentityKey; #[derive(Debug)] pub struct DelegationTxs {} @@ -17,7 +18,7 @@ impl AppView for DelegationTxs { sqlx::query( "CREATE TABLE stake_delegation_txs ( id SERIAL PRIMARY KEY, - validator_ik BYTEA NOT NULL, + ik TEXT NOT NULL, amount BIGINT NOT NULL, height BIGINT NOT NULL, tx_hash BYTEA NOT NULL @@ -26,8 +27,8 @@ impl AppView for DelegationTxs { .execute(dbtx.as_mut()) .await?; - // Create index on validator_ik - sqlx::query("CREATE INDEX idx_stake_delegation_txs_validator_ik ON stake_delegation_txs(validator_ik);") + // Create index on ik + sqlx::query("CREATE INDEX idx_stake_delegation_txs_ik ON stake_delegation_txs(ik);") .execute(dbtx.as_mut()) .await?; @@ -38,8 +39,8 @@ impl AppView for DelegationTxs { .execute(dbtx.as_mut()) .await?; - // Create composite index on validator_ik and height (descending) - sqlx::query("CREATE INDEX idx_stake_delegation_txs_validator_ik_height ON stake_delegation_txs(validator_ik, height DESC);") + // Create composite index on ik and height (descending) + sqlx::query("CREATE INDEX idx_stake_delegation_txs_validator_ik_height ON stake_delegation_txs(ik, height DESC);") .execute(dbtx.as_mut()) .await?; @@ -58,10 +59,10 @@ impl AppView for DelegationTxs { ) -> Result<()> { let pe = pb::EventDelegate::from_event(event.as_ref())?; - let ik_bytes = pe + let ik: IdentityKey = pe .identity_key .ok_or_else(|| anyhow::anyhow!("missing ik in event"))? - .ik; + .try_into()?; let amount = Amount::try_from( pe.amount @@ -69,9 +70,9 @@ impl AppView for DelegationTxs { )?; sqlx::query( - "INSERT INTO stake_delegation_txs (validator_ik, amount, height, tx_hash) VALUES ($1, $2, $3, $4)" + "INSERT INTO stake_delegation_txs (ik, amount, height, tx_hash) VALUES ($1, $2, $3, $4)" ) - .bind(&ik_bytes) + .bind(ik.to_string()) .bind(amount.value() as i64) .bind(event.block_height as i64) .bind(event.tx_hash.ok_or_else(|| anyhow!("missing tx hash in event"))?) diff --git a/crates/bin/pindexer/src/stake/missed_blocks.rs b/crates/bin/pindexer/src/stake/missed_blocks.rs index 7b5115792c..4516d619ac 100644 --- a/crates/bin/pindexer/src/stake/missed_blocks.rs +++ b/crates/bin/pindexer/src/stake/missed_blocks.rs @@ -2,6 +2,7 @@ use anyhow::Result; use cometindex::{async_trait, sqlx, AppView, ContextualizedEvent, PgPool, PgTransaction}; use penumbra_proto::{core::component::stake::v1 as pb, event::ProtoEvent}; +use penumbra_stake::IdentityKey; #[derive(Debug)] pub struct MissedBlocks {} @@ -18,7 +19,7 @@ impl AppView for MissedBlocks { "CREATE TABLE stake_missed_blocks ( id SERIAL PRIMARY KEY, height BIGINT NOT NULL, - ik BYTEA NOT NULL + ik TEXT NOT NULL );", ) .execute(dbtx.as_mut()) @@ -55,16 +56,16 @@ impl AppView for MissedBlocks { _src_db: &PgPool, ) -> Result<(), anyhow::Error> { let pe = pb::EventValidatorMissedBlock::from_event(event.as_ref())?; - let ik_bytes = pe + let ik: IdentityKey = pe .identity_key .ok_or_else(|| anyhow::anyhow!("missing ik in event"))? - .ik; + .try_into()?; let height = event.block_height; sqlx::query("INSERT INTO stake_missed_blocks (height, ik) VALUES ($1, $2)") .bind(height as i64) - .bind(ik_bytes) + .bind(ik.to_string()) .execute(dbtx.as_mut()) .await?; diff --git a/crates/bin/pindexer/src/stake/slashings.rs b/crates/bin/pindexer/src/stake/slashings.rs index 1be89b6944..bbc472171f 100644 --- a/crates/bin/pindexer/src/stake/slashings.rs +++ b/crates/bin/pindexer/src/stake/slashings.rs @@ -18,7 +18,7 @@ impl AppView for Slashings { "CREATE TABLE stake_slashings ( id SERIAL PRIMARY KEY, height BIGINT NOT NULL, - ik BYTEA NOT NULL, + ik TEXT NOT NULL, epoch_index BIGINT NOT NULL, penalty TEXT NOT NULL );", @@ -73,7 +73,7 @@ impl AppView for Slashings { VALUES ($1, $2, $3, $4)", ) .bind(height as i64) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(epoch_index as i64) .bind(penalty_json) .execute(dbtx.as_mut()) diff --git a/crates/bin/pindexer/src/stake/undelegation_txs.rs b/crates/bin/pindexer/src/stake/undelegation_txs.rs index d4f9da26e4..aeb9d90511 100644 --- a/crates/bin/pindexer/src/stake/undelegation_txs.rs +++ b/crates/bin/pindexer/src/stake/undelegation_txs.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use cometindex::{async_trait, sqlx, AppView, ContextualizedEvent, PgPool, PgTransaction}; use penumbra_num::Amount; use penumbra_proto::{core::component::stake::v1 as pb, event::ProtoEvent}; +use penumbra_stake::IdentityKey; #[derive(Debug)] pub struct UndelegationTxs {} @@ -17,7 +18,7 @@ impl AppView for UndelegationTxs { sqlx::query( "CREATE TABLE stake_undelegation_txs ( id SERIAL PRIMARY KEY, - validator_ik BYTEA NOT NULL, + ik TEXT NOT NULL, amount BIGINT NOT NULL, height BIGINT NOT NULL, tx_hash BYTEA NOT NULL @@ -26,8 +27,8 @@ impl AppView for UndelegationTxs { .execute(dbtx.as_mut()) .await?; - // Create index on validator_ik - sqlx::query("CREATE INDEX idx_stake_undelegation_txs_validator_ik ON stake_undelegation_txs(validator_ik);") + // Create index on ik + sqlx::query("CREATE INDEX idx_stake_undelegation_txs_ik ON stake_undelegation_txs(ik);") .execute(dbtx.as_mut()) .await?; @@ -38,8 +39,8 @@ impl AppView for UndelegationTxs { .execute(dbtx.as_mut()) .await?; - // Create composite index on validator_ik and height (descending) - sqlx::query("CREATE INDEX idx_stake_undelegation_txs_validator_ik_height ON stake_undelegation_txs(validator_ik, height DESC);") + // Create composite index on ik and height (descending) + sqlx::query("CREATE INDEX idx_stake_undelegation_txs_ik_height ON stake_undelegation_txs(ik, height DESC);") .execute(dbtx.as_mut()) .await?; @@ -58,10 +59,10 @@ impl AppView for UndelegationTxs { ) -> Result<()> { let pe = pb::EventUndelegate::from_event(event.as_ref())?; - let ik_bytes = pe + let ik: IdentityKey = pe .identity_key .ok_or_else(|| anyhow::anyhow!("missing ik in event"))? - .ik; + .try_into()?; let amount = Amount::try_from( pe.amount @@ -69,9 +70,9 @@ impl AppView for UndelegationTxs { )?; sqlx::query( - "INSERT INTO stake_undelegation_txs (validator_ik, amount, height, tx_hash) VALUES ($1, $2, $3, $4)" + "INSERT INTO stake_undelegation_txs (ik, amount, height, tx_hash) VALUES ($1, $2, $3, $4)" ) - .bind(&ik_bytes) + .bind(ik.to_string()) .bind(amount.value() as i64) .bind(event.block_height as i64) .bind(event.tx_hash.ok_or_else(|| anyhow!("missing tx hash in event"))?) diff --git a/crates/bin/pindexer/src/stake/validator_set.rs b/crates/bin/pindexer/src/stake/validator_set.rs index 67472ab403..fd0abf344c 100644 --- a/crates/bin/pindexer/src/stake/validator_set.rs +++ b/crates/bin/pindexer/src/stake/validator_set.rs @@ -28,7 +28,7 @@ impl AppView for ValidatorSet { // hence TEXT fields "CREATE TABLE stake_validator_set ( id SERIAL PRIMARY KEY, - ik BYTEA NOT NULL, + ik TEXT NOT NULL, name TEXT NOT NULL, definition TEXT NOT NULL, voting_power BIGINT NOT NULL, @@ -181,7 +181,7 @@ async fn add_genesis_validators<'a>( ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", ) - .bind(val.identity_key.to_bytes()) + .bind(val.identity_key.to_string()) .bind(val.name.clone()) .bind(serde_json::to_string(&val).expect("can serialize")) .bind(delegation_amount.value() as i64) @@ -200,7 +200,7 @@ async fn handle_upload<'a>(dbtx: &mut PgTransaction<'a>, val: Validator) -> Resu // First, check if the validator already exists let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM stake_validator_set WHERE ik = $1)") - .bind(&val.identity_key.to_bytes()) + .bind(&val.identity_key.to_string()) .fetch_one(dbtx.as_mut()) .await?; @@ -212,7 +212,7 @@ async fn handle_upload<'a>(dbtx: &mut PgTransaction<'a>, val: Validator) -> Resu definition = $3 WHERE ik = $1", ) - .bind(val.identity_key.to_bytes()) + .bind(val.identity_key.to_string()) .bind(val.name.clone()) .bind(serde_json::to_string(&val).expect("can serialize")) .execute(dbtx.as_mut()) @@ -226,7 +226,7 @@ async fn handle_upload<'a>(dbtx: &mut PgTransaction<'a>, val: Validator) -> Resu ) VALUES ($1, $2, $3, 0, 0, 0, $4, $5)", ) - .bind(val.identity_key.to_bytes()) + .bind(val.identity_key.to_string()) .bind(val.name.clone()) .bind(serde_json::to_string(&val).expect("can serialize")) .bind(serde_json::to_string(&validator::State::Defined).expect("can serialize")) // ValidatorManager::add_validator @@ -250,7 +250,7 @@ async fn handle_delegate<'a>( queued_delegations = queued_delegations + $2 WHERE ik = $1", ) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(amount.value() as i64) .execute(dbtx.as_mut()) .await? @@ -276,7 +276,7 @@ async fn handle_undelegate<'a>( queued_undelegations = queued_undelegations + $2 WHERE ik = $1", ) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(amount.value() as i64) .execute(dbtx.as_mut()) .await? @@ -304,7 +304,7 @@ async fn handle_voting_power_change<'a>( queued_undelegations = 0 WHERE ik = $1", ) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(voting_power.value() as i64) .execute(dbtx.as_mut()) .await? @@ -330,7 +330,7 @@ async fn handle_validator_state_change<'a>( validator_state = $2 WHERE ik = $1", ) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(serde_json::to_string(&state).expect("can serialize")) .execute(dbtx.as_mut()) .await? @@ -356,7 +356,7 @@ async fn handle_validator_bonding_state_change<'a>( bonding_state = $2 WHERE ik = $1", ) - .bind(ik.to_bytes()) + .bind(ik.to_string()) .bind(serde_json::to_string(&bonding_state).expect("can serialize")) .execute(dbtx.as_mut()) .await? diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 6e23fc5c3f..f41da7426e 100644 Binary files a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs and b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/crates/core/component/governance/src/action_handler/validator_vote.rs b/crates/core/component/governance/src/action_handler/validator_vote.rs index caed7b3547..68f9f100a3 100644 --- a/crates/core/component/governance/src/action_handler/validator_vote.rs +++ b/crates/core/component/governance/src/action_handler/validator_vote.rs @@ -114,6 +114,7 @@ impl ActionHandler for ValidatorVote { } } + // Get the validator's voting power for the proposal let voting_power = state .specific_validator_voting_power_at_proposal_start(*proposal, *identity_key) .await?; diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 294b190c53..049d277678 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/flake.nix b/flake.nix index dc1b7ee299..1db398d52a 100644 --- a/flake.nix +++ b/flake.nix @@ -76,7 +76,7 @@ [clang openssl rocksdb]; inherit system PKG_CONFIG_PATH LIBCLANG_PATH ROCKSDB_LIB_DIR; - cargoExtraArgs = "-p pd -p pcli -p pclientd"; + cargoExtraArgs = "-p pd -p pcli -p pclientd -p pindexer"; meta = { description = "A fully private proof-of-stake network and decentralized exchange for the Cosmos ecosystem"; homepage = "https://penumbra.zone"; @@ -130,6 +130,8 @@ pcli.program = "${penumbra}/bin/pcli"; pclientd.type = "app"; pclientd.program = "${penumbra}/bin/pclientd"; + pindexer.type = "app"; + pindexer.program = "${penumbra}/bin/pindexer"; cometbft.type = "app"; cometbft.program = "${cometbft}/bin/cometbft"; };