use super::postgres_epoch::PostgresEpoch; use super::postgres_session::PostgresSession; use log::{debug, warn}; use solana_lite_rpc_core::structures::epoch::EpochRef; use solana_lite_rpc_core::structures::produced_block::TransactionInfo; use solana_lite_rpc_core::{encoding::BASE64, structures::produced_block::ProducedBlock}; use solana_sdk::clock::Slot; use solana_sdk::commitment_config::CommitmentConfig; use solana_transaction_status::Reward; use std::time::Instant; use tokio_postgres::types::ToSql; #[derive(Debug)] pub struct PostgresBlock { pub slot: i64, pub blockhash: String, pub block_height: i64, pub parent_slot: i64, pub block_time: i64, pub previous_blockhash: String, pub rewards: Option, pub leader_id: Option, } impl From<&ProducedBlock> for PostgresBlock { fn from(value: &ProducedBlock) -> Self { let rewards = value .rewards .as_ref() .map(|x| BASE64.serialize::>(x).ok()) .unwrap_or(None); Self { blockhash: value.blockhash.clone(), block_height: value.block_height as i64, slot: value.slot as i64, parent_slot: value.parent_slot as i64, block_time: value.block_time as i64, previous_blockhash: value.previous_blockhash.clone(), // TODO add leader_id, etc. rewards, leader_id: value.leader_id.clone(), } } } impl PostgresBlock { pub fn to_produced_block( &self, transaction_infos: Vec, commitment_config: CommitmentConfig, ) -> ProducedBlock { let rewards_vec: Option> = self .rewards .as_ref() .map(|x| BASE64.deserialize::>(x).ok()) .unwrap_or(None); ProducedBlock { // TODO implement transactions: transaction_infos, leader_id: None, blockhash: self.blockhash.clone(), block_height: self.block_height as u64, slot: self.slot as Slot, parent_slot: self.parent_slot as Slot, block_time: self.block_time as u64, commitment_config, previous_blockhash: self.previous_blockhash.clone(), rewards: rewards_vec, } } } impl PostgresBlock { pub fn build_create_table_statement(epoch: EpochRef) -> String { let schema = PostgresEpoch::build_schema_name(epoch); format!( r#" CREATE TABLE IF NOT EXISTS {schema}.blocks ( slot BIGINT NOT NULL, blockhash TEXT NOT NULL, leader_id TEXT, block_height BIGINT NOT NULL, parent_slot BIGINT NOT NULL, block_time BIGINT NOT NULL, previous_blockhash TEXT NOT NULL, rewards TEXT, CONSTRAINT pk_block_slot PRIMARY KEY(slot) ) WITH (FILLFACTOR=90); CLUSTER {schema}.blocks USING pk_block_slot; "#, schema = schema ) } pub fn build_query_statement(epoch: EpochRef, slot: Slot) -> String { format!( r#" SELECT slot, blockhash, block_height, parent_slot, block_time, previous_blockhash, rewards, leader_id, {epoch}::bigint as _epoch, '{schema}'::text as _epoch_schema FROM {schema}.blocks WHERE slot = {slot} "#, schema = PostgresEpoch::build_schema_name(epoch), epoch = epoch, slot = slot ) } // true is actually inserted; false if operation was noop pub async fn save( &self, postgres_session: &PostgresSession, epoch: EpochRef, ) -> anyhow::Result { const NB_ARGUMENTS: usize = 8; let started = Instant::now(); let schema = PostgresEpoch::build_schema_name(epoch); let values = PostgresSession::values_vec(NB_ARGUMENTS, &[]); let statement = format!( r#" INSERT INTO {schema}.blocks (slot, blockhash, block_height, parent_slot, block_time, previous_blockhash, rewards, leader_id) VALUES {} -- prevent updates ON CONFLICT DO NOTHING RETURNING ( -- get previous max slot SELECT max(all_blocks.slot) as prev_max_slot FROM {schema}.blocks AS all_blocks WHERE all_blocks.slot!={schema}.blocks.slot ) "#, values, schema = schema, ); let mut args: Vec<&(dyn ToSql + Sync)> = Vec::with_capacity(NB_ARGUMENTS); args.push(&self.slot); args.push(&self.blockhash); args.push(&self.block_height); args.push(&self.parent_slot); args.push(&self.block_time); args.push(&self.previous_blockhash); args.push(&self.rewards); args.push(&self.leader_id); let returning = postgres_session .execute_and_return(&statement, &args) .await?; // TODO: decide what to do if block already exists match returning { Some(row) => { // check if monotonic let prev_max_slot = row.get::<&str, Option>("prev_max_slot"); // None -> no previous rows debug!("Inserted block {}", self.slot,); if let Some(prev_max_slot) = prev_max_slot { if prev_max_slot > self.slot { // note: unclear if this is desired behavior! warn!( "Block {} was inserted behind tip of highest slot number {} (epoch {})", self.slot, prev_max_slot, epoch ); } } } None => { // database detected conflict warn!("Block {} already exists - not updated", self.slot); return Ok(false); } } debug!( "Inserting block {} to schema {} postgres took {:.2}ms", self.slot, schema, started.elapsed().as_secs_f64() * 1000.0 ); Ok(true) } } #[cfg(test)] mod tests { use super::*; use solana_sdk::commitment_config::CommitmentConfig; #[test] fn map_postgresblock_to_produced_block() { let block = PostgresBlock { slot: 5050505, blockhash: "blockhash".to_string(), block_height: 4040404, parent_slot: 5050500, block_time: 12121212, previous_blockhash: "previous_blockhash".to_string(), rewards: None, leader_id: None, }; let transaction_infos = vec![create_tx_info(), create_tx_info()]; let produced_block = block.to_produced_block(transaction_infos, CommitmentConfig::confirmed()); assert_eq!(produced_block.slot, 5050505); assert_eq!(produced_block.transactions.len(), 2); } fn create_tx_info() -> TransactionInfo { TransactionInfo { signature: "signature".to_string(), is_vote: false, err: None, cu_requested: None, prioritization_fees: None, cu_consumed: None, recent_blockhash: "recent_blockhash".to_string(), message: "message".to_string(), writable_accounts: vec![], readable_accounts: vec![], } } }