From 750ff580ce018aaecd70b54ba7639028f07a0b4b Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 2 Sep 2025 17:56:17 -0400 Subject: [PATCH 01/44] feat: first pass at a chain store --- Cargo.lock | 15 ++ Cargo.toml | 1 + modules/chain_store/Cargo.toml | 23 ++ modules/chain_store/src/chain_store.rs | 13 ++ modules/chain_store/src/stores/fjall.rs | 275 ++++++++++++++++++++++++ modules/chain_store/src/stores/mod.rs | 26 +++ 6 files changed, 353 insertions(+) create mode 100644 modules/chain_store/Cargo.toml create mode 100644 modules/chain_store/src/chain_store.rs create mode 100644 modules/chain_store/src/stores/fjall.rs create mode 100644 modules/chain_store/src/stores/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 38eda229..c9ded7e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "acropolis_module_chain_store" +version = "0.1.0" +dependencies = [ + "acropolis_common", + "anyhow", + "caryatid_sdk", + "config", + "fjall", + "hex", + "minicbor 0.26.5", + "pallas-traverse", + "tempfile", +] + [[package]] name = "acropolis_module_drdd_state" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2eaec95d..ec2b154b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "modules/stake_delta_filter", # Filters address deltas "modules/epoch_activity_counter", # Counts fees and block producers for rewards "modules/accounts_state", # Tracks stake and reward accounts + "modules/chain_store", # Tracks historical information about blocks and TXs # Process builds "processes/omnibus", # All-inclusive omnibus process diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml new file mode 100644 index 00000000..05fab792 --- /dev/null +++ b/modules/chain_store/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "acropolis_module_chain_store" +version = "0.1.0" +edition = "2021" +authors = ["Simon Gellis "] +description = "Chain Store Tracker" +license = "Apache-2.0" + +[dependencies] +caryatid_sdk = "0.12" +acropolis_common = { path = "../../common" } +anyhow = "1.0" +config = "0.15.11" +fjall = "2.7.0" +hex = "0.4" +minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } +pallas-traverse = "0.32.1" + +[dev-dependencies] +tempfile = "3" + +[lib] +path = "src/chain_store.rs" \ No newline at end of file diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs new file mode 100644 index 00000000..8b0a3b64 --- /dev/null +++ b/modules/chain_store/src/chain_store.rs @@ -0,0 +1,13 @@ +mod stores; + +use acropolis_common::messages::Message; +use caryatid_sdk::{module, Context, Module}; +use config::Config; +use std::sync::Arc; + +#[module( + message_type(Message), + name = "chain-store", + description = "Block and TX state" +)] +pub struct ChainStore; diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs new file mode 100644 index 00000000..f0e5792a --- /dev/null +++ b/modules/chain_store/src/stores/fjall.rs @@ -0,0 +1,275 @@ +use std::{collections::HashMap, path::Path, sync::Arc}; + +use acropolis_common::{BlockInfo, TxHash}; +use anyhow::{bail, Result}; +use config::Config; +use fjall::{Batch, Keyspace, Partition}; + +pub struct FjallStore { + keyspace: Keyspace, + blocks: FjallBlockStore, + txs: FjallTXStore, +} + +const DEFAULT_DATABASE_PATH: &str = "fjall-blocks"; +const BLOCKS_PARTITION: &str = "blocks"; +const BLOCK_HASHES_BY_SLOT_PARTITION: &str = "block-hashes-by-slot"; +const BLOCK_HASHES_BY_NUMBER_PARTITION: &str = "block-hashes-by-number"; +const TXS_PARTITION: &str = "txs"; + +impl FjallStore { + pub fn new(config: Arc) -> Result { + let path = config.get_string("database-path").unwrap_or(DEFAULT_DATABASE_PATH.to_string()); + let fjall_config = fjall::Config::new(Path::new(&path)); + let keyspace = fjall_config.open()?; + let blocks = FjallBlockStore::new(&keyspace)?; + let txs = FjallTXStore::new(&keyspace)?; + Ok(Self { + keyspace, + blocks, + txs, + }) + } +} + +impl super::Store for FjallStore { + fn insert_block(&self, block: &super::Block) -> Result<()> { + let mut txs_with_hash = Vec::with_capacity(block.txs.len()); + let mut persisted_block = PersistedBlock { + slot: block.slot, + number: block.number, + hash: block.hash.clone(), + tx_hashes: vec![], + }; + for tx in &block.txs { + let hash = super::hash_tx(tx)?; + persisted_block.tx_hashes.push(hash); + txs_with_hash.push((tx, hash)); + } + + let mut batch = self.keyspace.batch(); + self.blocks.insert(&mut batch, &persisted_block); + for (tx, hash) in txs_with_hash { + self.txs.insert_tx(&mut batch, tx, hash); + } + + batch.commit()?; + + Ok(()) + } + + fn get_block_by_hash(&self, hash: &[u8]) -> Result { + let block = self.blocks.get_by_hash(hash)?; + self.txs.hydrate_block(block) + } + + fn get_block_by_slot(&self, slot: u64) -> Result { + let block = self.blocks.get_by_slot(slot)?; + self.txs.hydrate_block(block) + } + + fn get_block_by_number(&self, number: u64) -> Result { + let block = self.blocks.get_by_number(number)?; + self.txs.hydrate_block(block) + } + + fn get_latest_block(&self) -> Result { + let block = self.blocks.get_latest()?; + self.txs.hydrate_block(block) + } +} + +struct FjallBlockStore { + blocks: Partition, + block_hashes_by_slot: Partition, + block_hashes_by_number: Partition, +} + +impl FjallBlockStore { + fn new(keyspace: &Keyspace) -> Result { + let blocks = + keyspace.open_partition(BLOCKS_PARTITION, fjall::PartitionCreateOptions::default())?; + let block_hashes_by_slot = keyspace.open_partition( + BLOCK_HASHES_BY_SLOT_PARTITION, + fjall::PartitionCreateOptions::default(), + )?; + let block_hashes_by_number = keyspace.open_partition( + BLOCK_HASHES_BY_NUMBER_PARTITION, + fjall::PartitionCreateOptions::default(), + )?; + Ok(Self { + blocks, + block_hashes_by_slot, + block_hashes_by_number, + }) + } + + fn insert(&self, batch: &mut Batch, block: &PersistedBlock) { + let encoded = { + let mut bytes = vec![]; + minicbor::encode(block, &mut bytes).expect("infallible"); + bytes + }; + batch.insert(&self.blocks, &block.hash, encoded); + batch.insert( + &self.block_hashes_by_slot, + block.slot.to_be_bytes(), + &block.hash, + ); + batch.insert( + &self.block_hashes_by_number, + block.number.to_be_bytes(), + &block.hash, + ); + } + + fn get_by_hash(&self, hash: &[u8]) -> Result { + let Some(block) = self.blocks.get(hash)? else { + bail!("No block found with hash {}", hex::encode(hash)); + }; + Ok(minicbor::decode(&block)?) + } + + fn get_by_slot(&self, slot: u64) -> Result { + let Some(hash) = self.block_hashes_by_slot.get(slot.to_be_bytes())? else { + bail!("No block found for slot {slot}"); + }; + self.get_by_hash(&hash) + } + + fn get_by_number(&self, number: u64) -> Result { + let Some(hash) = self.block_hashes_by_number.get(number.to_be_bytes())? else { + bail!("No block found with number {number}"); + }; + self.get_by_hash(&hash) + } + + fn get_latest(&self) -> Result { + let Some((_, hash)) = self.block_hashes_by_slot.last_key_value()? else { + bail!("No blocks found"); + }; + self.get_by_hash(&hash) + } +} + +#[derive(minicbor::Decode, minicbor::Encode)] +struct PersistedBlock { + #[n(0)] + slot: u64, + #[n(1)] + number: u64, + #[b(2)] + hash: Vec, + #[b(3)] + tx_hashes: Vec, +} + +struct FjallTXStore { + txs: Partition, +} +impl FjallTXStore { + fn new(keyspace: &Keyspace) -> Result { + let txs = + keyspace.open_partition(TXS_PARTITION, fjall::PartitionCreateOptions::default())?; + Ok(Self { txs }) + } + + fn insert_tx(&self, batch: &mut Batch, tx: &[u8], hash: TxHash) { + batch.insert(&self.txs, hash, tx); + } + + fn hydrate_block(&self, block: PersistedBlock) -> Result { + let mut txs = vec![]; + for hash in block.tx_hashes { + let Some(tx) = self.txs.get(hash)? else { + bail!("Could not find TX {}", hex::encode(hash)); + }; + txs.push(tx.to_vec()); + } + Ok(super::Block { + slot: block.slot, + number: block.number, + hash: block.hash, + txs, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::stores::{Block, Store}; + + use super::*; + use tempfile::TempDir; + + const TEST_TX: &str = "84a500d90102828258200000000000000000000000000000000000000000000000000000000000000000008258200000000000000000000000000000000000000000000000000000000000000000050183a300583930be4d215663909bb5935b923c2df611723480935bb4722d5f152b646a7467ae52afc8e9f5603c9265e7ce24853863a34f6b12d12a098f880801821a002dc6c0a3581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e15a144555344431a01312d00581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa15820000de140f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc601581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a351535183a14574494e44591a01312d00028201d81858f3d8799f581cf6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc69f9f581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e154455534443ff9f581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a3515351834574494e4459ffff1a01312d000505d87a80051a002dc6c0d8799f581c60c5ca218d3fa6ba7ecf4697a7a566ead9feb87068fc1229eddcf287ffd8799fd8799fa1581c633a136877ed6ad0ab33e69a22611319673474c8bd0a79a4c76d9289a158200014df10a933477ea168013e2b5af4a9e029e36d26738eb6dfe382e1f3eab3e21a05f5e100d87a80ffffffa300581d60035dee66d57cc271697711d63c8c35ffa0b6c4468a6a98024feac73b01821a001e8480a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa15820000643b0f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc601028201d81843d8798082583900c279a3fb3b4e62bbc78e288783b58045d4ae82a18867d8352d02775a121fd22e0b57ac206fefc763f8bfa0771919f5218b40691eea4514d0821a001e8480a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa158200014df10f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc61a01312d00020009a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa35820000643b0f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc6015820000de140f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc60158200014df10f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc61a01312d0012d901028982582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0082582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0182582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0282582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0382582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b50082582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b50182582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b5028258200e6d53b393d19cfbd2307b104b4822d9267792493c58a480c8ea69eca8dd2ce20082582045ae0839622478c3ed2fbf5eea03c54ca3fd57607b7a2660445166ea8a42d98c00a200d9010281825820000000000000000000000000000000000000000000000000000000000000000058400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a182010082d87a9f9f9f581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e154455534443ff9f581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a3515351834574494e4459ffff0001ff821a00d59f801b00000002540be400f5f6"; + + fn test_block() -> Block { + let tx = hex::decode(TEST_TX).unwrap(); + Block { + number: 1, + slot: 3, + hash: vec![0xca, 0xfe, 0xd0, 0x0d], + txs: vec![tx], + } + } + + struct TestState { + #[expect(unused)] + dir: TempDir, + store: FjallStore, + } + + fn init_state() -> TestState { + let dir = tempfile::tempdir().unwrap(); + let dir_name = dir.path().to_str().expect("dir_name cannot be stored as string"); + let config = + Config::builder().set_default("database-path", dir_name).unwrap().build().unwrap(); + let store = FjallStore::new(Arc::new(config)).unwrap(); + TestState { dir, store } + } + + #[test] + fn should_get_block_by_hash() { + let state = init_state(); + let block = test_block(); + + state.store.insert_block(&block).unwrap(); + + let new_block = state.store.get_block_by_hash(&block.hash).unwrap(); + assert_eq!(block, new_block); + } + + #[test] + fn should_get_block_by_slot() { + let state = init_state(); + let block = test_block(); + + state.store.insert_block(&block).unwrap(); + + let new_block = state.store.get_block_by_slot(block.slot).unwrap(); + assert_eq!(block, new_block); + } + + #[test] + fn should_get_block_by_number() { + let state = init_state(); + let block = test_block(); + + state.store.insert_block(&block).unwrap(); + + let new_block = state.store.get_block_by_number(block.number).unwrap(); + assert_eq!(block, new_block); + } + + #[test] + fn should_get_latest_block() { + let state = init_state(); + let block = test_block(); + + state.store.insert_block(&block).unwrap(); + + let new_block = state.store.get_latest_block().unwrap(); + assert_eq!(block, new_block); + } +} diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs new file mode 100644 index 00000000..30889229 --- /dev/null +++ b/modules/chain_store/src/stores/mod.rs @@ -0,0 +1,26 @@ +use acropolis_common::TxHash; +use anyhow::{Context, Result}; + +mod fjall; + +pub trait Store { + fn insert_block(&self, block: &Block) -> Result<()>; + + fn get_block_by_hash(&self, hash: &[u8]) -> Result; + fn get_block_by_slot(&self, slot: u64) -> Result; + fn get_block_by_number(&self, number: u64) -> Result; + fn get_latest_block(&self) -> Result; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Block { + slot: u64, + number: u64, + hash: Vec, + txs: Vec>, +} + +pub(crate) fn hash_tx(tx: &[u8]) -> Result { + let tx = pallas_traverse::MultiEraTx::decode(tx).context("could not decode tx")?; + Ok(TxHash::from(*tx.hash())) +} From 49d08888858be839a84ea49d85f24cebe71ffd64 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 3 Sep 2025 16:22:59 -0400 Subject: [PATCH 02/44] feat: wire block queries up to API --- Cargo.lock | 1 + common/src/queries/blocks.rs | 32 +++----- modules/chain_store/Cargo.toml | 1 + modules/chain_store/src/chain_store.rs | 98 ++++++++++++++++++++++++- modules/chain_store/src/stores/fjall.rs | 27 +------ modules/chain_store/src/stores/mod.rs | 17 ++++- 6 files changed, 124 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9ded7e7..47619688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,7 @@ dependencies = [ "minicbor 0.26.5", "pallas-traverse", "tempfile", + "tracing", ] [[package]] diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 3a0ffec3..7c9b46ee 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,3 +1,6 @@ +pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = + ("blocks-state-query-topic", "cardano.query.blocks"); + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlocksStateQuery { GetLatestBlock, @@ -6,8 +9,8 @@ pub enum BlocksStateQuery { GetBlockInfo { block_key: Vec }, GetNextBlocks { block_key: Vec }, GetPreviousBlocks { block_key: Vec }, - GetBlockBySlot { slot_key: Vec }, - GetBlockByEpochSlot { slot_key: Vec }, + GetBlockBySlot { slot: u64 }, + GetBlockByEpochSlot { epoch: u64, slot: u64 }, GetBlockTransactions { block_key: Vec }, GetBlockTransactionsCBOR { block_key: Vec }, GetBlockInvolvedAddresses { block_key: Vec }, @@ -15,14 +18,14 @@ pub enum BlocksStateQuery { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlocksStateQueryResponse { - LatestBlock(LatestBlock), - LatestBlockTransactions(LatestBlockTransactions), - LatestBlockTransactionsCBOR(LatestBlockTransactionsCBOR), + LatestBlock(BlockInfo), + LatestBlockTransactions(BlockTransactions), + LatestBlockTransactionsCBOR(BlockTransactionsCBOR), BlockInfo(BlockInfo), NextBlocks(NextBlocks), PreviousBlocks(PreviousBlocks), - BlockBySlot(BlockBySlot), - BlockByEpochSlot(BlockByEpochSlot), + BlockBySlot(BlockInfo), + BlockByEpochSlot(BlockInfo), BlockTransactions(BlockTransactions), BlockTransactionsCBOR(BlockTransactionsCBOR), BlockInvolvedAddresses(BlockInvolvedAddresses), @@ -30,15 +33,6 @@ pub enum BlocksStateQueryResponse { Error(String), } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct LatestBlock {} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct LatestBlockTransactions {} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct LatestBlockTransactionsCBOR {} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BlockInfo {} @@ -48,12 +42,6 @@ pub struct NextBlocks {} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PreviousBlocks {} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockBySlot {} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockByEpochSlot {} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BlockTransactions {} diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index 05fab792..0c4b4443 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -15,6 +15,7 @@ fjall = "2.7.0" hex = "0.4" minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } pallas-traverse = "0.32.1" +tracing = "0.1.40" [dev-dependencies] tempfile = "3" diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 8b0a3b64..72bac539 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,9 +1,21 @@ mod stores; -use acropolis_common::messages::Message; +use acropolis_common::{ + messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, + queries::blocks::{ + BlockInfo, BlocksStateQuery, BlocksStateQueryResponse, DEFAULT_BLOCKS_QUERY_TOPIC, + }, +}; +use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; use std::sync::Arc; +use tracing::error; + +use crate::stores::{fjall::FjallStore, Block, Store}; + +const DEFAULT_TRANSACTIONS_TOPIC: &str = "cardano.txs"; +const DEFAULT_STORE: &str = "fjall"; #[module( message_type(Message), @@ -11,3 +23,87 @@ use std::sync::Arc; description = "Block and TX state" )] pub struct ChainStore; + +impl ChainStore { + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + let new_txs_topic = config + .get_string("transactions-topic") + .unwrap_or(DEFAULT_TRANSACTIONS_TOPIC.to_string()); + let block_queries_topic = config + .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) + .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); + + let store_type = config.get_string("store").unwrap_or(DEFAULT_STORE.to_string()); + let store: Arc = match store_type.as_str() { + "fjall" => Arc::new(FjallStore::new(config.clone())?), + _ => bail!("Unknown store type {store_type}"), + }; + + let query_store = store.clone(); + context.handle(&block_queries_topic, move |req| { + let query_store = query_store.clone(); + async move { + let Message::StateQuery(StateQuery::Blocks(query)) = req.as_ref() else { + return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error("Invalid message for blocks-state".into()), + ))); + }; + let res = Self::handle_blocks_query(&query_store, query) + .unwrap_or_else(|err| BlocksStateQueryResponse::Error(err.to_string())); + Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) + } + }); + + let mut new_txs_subscription = context.subscribe(&new_txs_topic).await?; + context.run(async move { + loop { + let Ok((_, message)) = new_txs_subscription.read().await else { + return; + }; + + if let Err(err) = Self::handle_new_txs(&store, &message) { + error!("Could not insert block: {err}"); + } + } + }); + + Ok(()) + } + + fn handle_new_txs(store: &Arc, message: &Message) -> Result<()> { + let Message::Cardano((info, CardanoMessage::ReceivedTxs(txs))) = message else { + bail!("Unexpected message type: {message:?}"); + }; + + let block = Block::from_info_and_txs(info, &txs.txs); + store.insert_block(&block) + } + + fn handle_blocks_query( + store: &Arc, + query: &BlocksStateQuery, + ) -> Result { + match query { + BlocksStateQuery::GetLatestBlock => { + let block = store.get_latest_block()?; + let info = Self::to_block_info(block); + Ok(BlocksStateQueryResponse::LatestBlock(info)) + } + BlocksStateQuery::GetBlockInfo { block_key } => { + let block = store.get_block_by_hash(block_key)?; + let info = Self::to_block_info(block); + Ok(BlocksStateQueryResponse::BlockInfo(info)) + } + BlocksStateQuery::GetBlockBySlot { slot } => { + let block = store.get_block_by_slot(*slot)?; + let info = Self::to_block_info(block); + Ok(BlocksStateQueryResponse::BlockBySlot(info)) + } + other => bail!("{other:?} not yet supported"), + } + } + + fn to_block_info(_block: Block) -> BlockInfo { + BlockInfo {} + } +} diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index f0e5792a..a4ed7f7c 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,6 +1,6 @@ -use std::{collections::HashMap, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; -use acropolis_common::{BlockInfo, TxHash}; +use acropolis_common::TxHash; use anyhow::{bail, Result}; use config::Config; use fjall::{Batch, Keyspace, Partition}; @@ -68,11 +68,6 @@ impl super::Store for FjallStore { self.txs.hydrate_block(block) } - fn get_block_by_number(&self, number: u64) -> Result { - let block = self.blocks.get_by_number(number)?; - self.txs.hydrate_block(block) - } - fn get_latest_block(&self) -> Result { let block = self.blocks.get_latest()?; self.txs.hydrate_block(block) @@ -137,13 +132,6 @@ impl FjallBlockStore { self.get_by_hash(&hash) } - fn get_by_number(&self, number: u64) -> Result { - let Some(hash) = self.block_hashes_by_number.get(number.to_be_bytes())? else { - bail!("No block found with number {number}"); - }; - self.get_by_hash(&hash) - } - fn get_latest(&self) -> Result { let Some((_, hash)) = self.block_hashes_by_slot.last_key_value()? else { bail!("No blocks found"); @@ -251,17 +239,6 @@ mod tests { assert_eq!(block, new_block); } - #[test] - fn should_get_block_by_number() { - let state = init_state(); - let block = test_block(); - - state.store.insert_block(&block).unwrap(); - - let new_block = state.store.get_block_by_number(block.number).unwrap(); - assert_eq!(block, new_block); - } - #[test] fn should_get_latest_block() { let state = init_state(); diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 30889229..5f790677 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -1,14 +1,13 @@ -use acropolis_common::TxHash; +use acropolis_common::{BlockInfo, TxHash}; use anyhow::{Context, Result}; -mod fjall; +pub mod fjall; -pub trait Store { +pub trait Store: Send + Sync { fn insert_block(&self, block: &Block) -> Result<()>; fn get_block_by_hash(&self, hash: &[u8]) -> Result; fn get_block_by_slot(&self, slot: u64) -> Result; - fn get_block_by_number(&self, number: u64) -> Result; fn get_latest_block(&self) -> Result; } @@ -19,6 +18,16 @@ pub struct Block { hash: Vec, txs: Vec>, } +impl Block { + pub fn from_info_and_txs(info: &BlockInfo, txs: &[Vec]) -> Self { + Self { + slot: info.slot, + number: info.number, + hash: info.hash.clone(), + txs: txs.to_vec(), + } + } +} pub(crate) fn hash_tx(tx: &[u8]) -> Result { let tx = pallas_traverse::MultiEraTx::decode(tx).context("could not decode tx")?; From 3bacc44b1e651c838c4df2b106091bc8dc7ceaaa Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 4 Sep 2025 11:54:18 -0400 Subject: [PATCH 03/44] feat: store block cbor --- modules/chain_store/src/chain_store.rs | 20 ++- modules/chain_store/src/stores/fjall.rs | 181 +++++++++++++----------- modules/chain_store/src/stores/mod.rs | 36 ++--- 3 files changed, 128 insertions(+), 109 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 72bac539..8df5c591 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -14,7 +14,7 @@ use tracing::error; use crate::stores::{fjall::FjallStore, Block, Store}; -const DEFAULT_TRANSACTIONS_TOPIC: &str = "cardano.txs"; +const DEFAULT_BLOCKS_TOPIC: &str = "cardano.block.body"; const DEFAULT_STORE: &str = "fjall"; #[module( @@ -26,9 +26,8 @@ pub struct ChainStore; impl ChainStore { pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - let new_txs_topic = config - .get_string("transactions-topic") - .unwrap_or(DEFAULT_TRANSACTIONS_TOPIC.to_string()); + let new_blocks_topic = + config.get_string("blocks-topic").unwrap_or(DEFAULT_BLOCKS_TOPIC.to_string()); let block_queries_topic = config .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); @@ -54,14 +53,14 @@ impl ChainStore { } }); - let mut new_txs_subscription = context.subscribe(&new_txs_topic).await?; + let mut new_blocks_subscription = context.subscribe(&new_blocks_topic).await?; context.run(async move { loop { - let Ok((_, message)) = new_txs_subscription.read().await else { + let Ok((_, message)) = new_blocks_subscription.read().await else { return; }; - if let Err(err) = Self::handle_new_txs(&store, &message) { + if let Err(err) = Self::handle_new_block(&store, &message) { error!("Could not insert block: {err}"); } } @@ -70,13 +69,12 @@ impl ChainStore { Ok(()) } - fn handle_new_txs(store: &Arc, message: &Message) -> Result<()> { - let Message::Cardano((info, CardanoMessage::ReceivedTxs(txs))) = message else { + fn handle_new_block(store: &Arc, message: &Message) -> Result<()> { + let Message::Cardano((info, CardanoMessage::BlockBody(body))) = message else { bail!("Unexpected message type: {message:?}"); }; - let block = Block::from_info_and_txs(info, &txs.txs); - store.insert_block(&block) + store.insert_block(info, &body.raw) } fn handle_blocks_query( diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index a4ed7f7c..e4f9d80e 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,10 +1,12 @@ use std::{path::Path, sync::Arc}; -use acropolis_common::TxHash; +use acropolis_common::{BlockInfo, TxHash}; use anyhow::{bail, Result}; use config::Config; use fjall::{Batch, Keyspace, Partition}; +use crate::stores::{Block, ExtraBlockData}; + pub struct FjallStore { keyspace: Keyspace, blocks: FjallBlockStore, @@ -33,24 +35,26 @@ impl FjallStore { } impl super::Store for FjallStore { - fn insert_block(&self, block: &super::Block) -> Result<()> { - let mut txs_with_hash = Vec::with_capacity(block.txs.len()); - let mut persisted_block = PersistedBlock { - slot: block.slot, - number: block.number, - hash: block.hash.clone(), - tx_hashes: vec![], + fn insert_block(&self, info: &BlockInfo, block: &[u8]) -> Result<()> { + let extra = ExtraBlockData { + epoch: info.epoch, + epoch_slot: info.epoch_slot, + timestamp: info.timestamp, + }; + let tx_hashes = super::extract_tx_hashes(block)?; + let raw = Block { + bytes: block.to_vec(), + extra, }; - for tx in &block.txs { - let hash = super::hash_tx(tx)?; - persisted_block.tx_hashes.push(hash); - txs_with_hash.push((tx, hash)); - } let mut batch = self.keyspace.batch(); - self.blocks.insert(&mut batch, &persisted_block); - for (tx, hash) in txs_with_hash { - self.txs.insert_tx(&mut batch, tx, hash); + self.blocks.insert(&mut batch, info, &raw); + for (index, hash) in tx_hashes.iter().enumerate() { + let tx_ref = StoredTransaction::BlockReference { + block_hash: info.hash.clone(), + index, + }; + self.txs.insert_tx(&mut batch, *hash, tx_ref); } batch.commit()?; @@ -58,19 +62,16 @@ impl super::Store for FjallStore { Ok(()) } - fn get_block_by_hash(&self, hash: &[u8]) -> Result { - let block = self.blocks.get_by_hash(hash)?; - self.txs.hydrate_block(block) + fn get_block_by_hash(&self, hash: &[u8]) -> Result { + self.blocks.get_by_hash(hash) } - fn get_block_by_slot(&self, slot: u64) -> Result { - let block = self.blocks.get_by_slot(slot)?; - self.txs.hydrate_block(block) + fn get_block_by_slot(&self, slot: u64) -> Result { + self.blocks.get_by_slot(slot) } - fn get_latest_block(&self) -> Result { - let block = self.blocks.get_latest()?; - self.txs.hydrate_block(block) + fn get_latest_block(&self) -> Result { + self.blocks.get_latest() } } @@ -99,40 +100,40 @@ impl FjallBlockStore { }) } - fn insert(&self, batch: &mut Batch, block: &PersistedBlock) { + fn insert(&self, batch: &mut Batch, info: &BlockInfo, raw: &Block) { let encoded = { let mut bytes = vec![]; - minicbor::encode(block, &mut bytes).expect("infallible"); + minicbor::encode(raw, &mut bytes).expect("infallible"); bytes }; - batch.insert(&self.blocks, &block.hash, encoded); + batch.insert(&self.blocks, &info.hash, encoded); batch.insert( &self.block_hashes_by_slot, - block.slot.to_be_bytes(), - &block.hash, + info.slot.to_be_bytes(), + &info.hash, ); batch.insert( &self.block_hashes_by_number, - block.number.to_be_bytes(), - &block.hash, + info.number.to_be_bytes(), + &info.hash, ); } - fn get_by_hash(&self, hash: &[u8]) -> Result { + fn get_by_hash(&self, hash: &[u8]) -> Result { let Some(block) = self.blocks.get(hash)? else { bail!("No block found with hash {}", hex::encode(hash)); }; Ok(minicbor::decode(&block)?) } - fn get_by_slot(&self, slot: u64) -> Result { + fn get_by_slot(&self, slot: u64) -> Result { let Some(hash) = self.block_hashes_by_slot.get(slot.to_be_bytes())? else { bail!("No block found for slot {slot}"); }; self.get_by_hash(&hash) } - fn get_latest(&self) -> Result { + fn get_latest(&self) -> Result { let Some((_, hash)) = self.block_hashes_by_slot.last_key_value()? else { bail!("No blocks found"); }; @@ -140,18 +141,6 @@ impl FjallBlockStore { } } -#[derive(minicbor::Decode, minicbor::Encode)] -struct PersistedBlock { - #[n(0)] - slot: u64, - #[n(1)] - number: u64, - #[b(2)] - hash: Vec, - #[b(3)] - tx_hashes: Vec, -} - struct FjallTXStore { txs: Partition, } @@ -162,43 +151,69 @@ impl FjallTXStore { Ok(Self { txs }) } - fn insert_tx(&self, batch: &mut Batch, tx: &[u8], hash: TxHash) { - batch.insert(&self.txs, hash, tx); + fn insert_tx(&self, batch: &mut Batch, hash: TxHash, tx: StoredTransaction) { + let bytes = minicbor::to_vec(tx).expect("infallible"); + batch.insert(&self.txs, hash, bytes); } +} - fn hydrate_block(&self, block: PersistedBlock) -> Result { - let mut txs = vec![]; - for hash in block.tx_hashes { - let Some(tx) = self.txs.get(hash)? else { - bail!("Could not find TX {}", hex::encode(hash)); - }; - txs.push(tx.to_vec()); - } - Ok(super::Block { - slot: block.slot, - number: block.number, - hash: block.hash, - txs, - }) - } +#[derive(minicbor::Decode, minicbor::Encode)] +enum StoredTransaction { + #[n(0)] + BlockReference { + #[n(0)] + block_hash: Vec, + #[n(1)] + index: usize, + }, + #[n(1)] + Inline { + #[n(0)] + bytes: Vec, + }, } #[cfg(test)] mod tests { - use crate::stores::{Block, Store}; + use crate::stores::Store; use super::*; + use pallas_traverse::{wellknown::GenesisValues, MultiEraBlock}; use tempfile::TempDir; - const TEST_TX: &str = "84a500d90102828258200000000000000000000000000000000000000000000000000000000000000000008258200000000000000000000000000000000000000000000000000000000000000000050183a300583930be4d215663909bb5935b923c2df611723480935bb4722d5f152b646a7467ae52afc8e9f5603c9265e7ce24853863a34f6b12d12a098f880801821a002dc6c0a3581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e15a144555344431a01312d00581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa15820000de140f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc601581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a351535183a14574494e44591a01312d00028201d81858f3d8799f581cf6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc69f9f581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e154455534443ff9f581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a3515351834574494e4459ffff1a01312d000505d87a80051a002dc6c0d8799f581c60c5ca218d3fa6ba7ecf4697a7a566ead9feb87068fc1229eddcf287ffd8799fd8799fa1581c633a136877ed6ad0ab33e69a22611319673474c8bd0a79a4c76d9289a158200014df10a933477ea168013e2b5af4a9e029e36d26738eb6dfe382e1f3eab3e21a05f5e100d87a80ffffffa300581d60035dee66d57cc271697711d63c8c35ffa0b6c4468a6a98024feac73b01821a001e8480a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa15820000643b0f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc601028201d81843d8798082583900c279a3fb3b4e62bbc78e288783b58045d4ae82a18867d8352d02775a121fd22e0b57ac206fefc763f8bfa0771919f5218b40691eea4514d0821a001e8480a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa158200014df10f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc61a01312d00020009a1581cbe4d215663909bb5935b923c2df611723480935bb4722d5f152b646aa35820000643b0f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc6015820000de140f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc60158200014df10f6a207f7eb0b2aca50c96d0b83b7b6cf0cb2161aa73648e8161ddcc61a01312d0012d901028982582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0082582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0182582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0282582016beda82efb0f2341fdb0bf6dec4a153b94681679826ae1e644070256601fcec0382582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b50082582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b50182582045394d375379204a64d3fd6987afa83d1dd0c4f14a36094056f136bc21ed07b5028258200e6d53b393d19cfbd2307b104b4822d9267792493c58a480c8ea69eca8dd2ce20082582045ae0839622478c3ed2fbf5eea03c54ca3fd57607b7a2660445166ea8a42d98c00a200d9010281825820000000000000000000000000000000000000000000000000000000000000000058400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a182010082d87a9f9f9f581c99b071ce8580d6a3a11b4902145adb8bfd0d2a03935af8cf66403e154455534443ff9f581cfa3eff2047fdf9293c5feef4dc85ce58097ea1c6da4845a3515351834574494e4459ffff0001ff821a00d59f801b00000002540be400f5f6"; + const TEST_BLOCK: &str = "820785828a1a0010afaa1a0150d7925820a22f65265e7a71cfc3b637d6aefe8f8241d562f5b1b787ff36697ae4c3886f185820e856c84a3d90c8526891bd58d957afadc522de37b14ae04c395db8a7a1b08c4a582015587d5633be324f8de97168399ab59d7113f0a74bc7412b81f7cc1007491671825840af9ff8cb146880eba1b12beb72d86be46fbc98f6b88110cd009bd6746d255a14bb0637e3a29b7204bff28236c1b9f73e501fed1eb5634bd741be120332d25e5e5850a9f1de24d01ba43b025a3351b25de50cc77f931ed8cdd0be632ad1a437ec9cf327b24eb976f91dbf68526f15bacdf8f0c1ea4a2072df9412796b34836a816760f4909b98c0e76b160d9aec6b2da060071903705820b5858c659096fcc19f2f3baef5fdd6198641a623bd43e792157b5ea3a2ecc85c8458200ca1ec2c1c2af308bd9e7a86eb12d603a26157752f3f71c337781c456e6ed0c90018a558408e554b644a2b25cb5892d07a26c273893829f1650ec33bf6809d953451c519c32cfd48d044cd897a17cdef154d5f5c9b618d9b54f8c49e170082c08c236524098209005901c05a96b747789ef6678b2f4a2a7caca92e270f736e9b621686f95dd1332005102faee21ed50cf6fa6c67e38b33df686c79c91d55f30769f7c964d98aa84cbefe0a808ee6f45faaf9badcc3f746e6a51df1aa979195871fd5ffd91037ea216803be7e7fccbf4c13038c459c7a14906ab57f3306fe155af7877c88866eede7935f642f6a72f1368c33ed5cc7607c995754af787a5af486958edb531c0ae65ce9fdce423ad88925e13ef78700950093ae707bb1100299a66a5bb15137f7ba62132ba1c9b74495aac50e1106bacb5db2bed4592f66b610c2547f485d061c6c149322b0c92bdde644eb672267fdab5533157ff398b9e16dd6a06edfd67151e18a3ac93fc28a51f9a73f8b867f5f432b1d9b5ae454ef63dea7e1a78631cf3fee1ba82db61726701ac5db1c4fee4bb6316768c82c0cdc4ebd58ccc686be882f9608592b3c718e4b5d356982a6b83433fe76d37394eff9f3a8e4773e3bab9a8b93b4ea90fa33bfbcf0dc5a21bfe64be2eefaa82c0494ab729e50596110f60ae9ad64b3eb9ddb54001b03cc264b65634c071d3b24a44322f39a9eae239fd886db8d429969433cb2d0a82d7877f174b0e154262f1af44ce5bc053b62daadd2926f957440ff3981a600d9010281825820af09d312a642fecb47da719156517bec678469c15789bcf002ce2ef563edf54200018182581d6052e63f22c5107ed776b70f7b92248b02552fd08f3e747bc745099441821b00000001373049f4a1581c34250edd1e9836f5378702fbf9416b709bc140e04f668cc355208518a1494154414441636f696e1953a6021a000306b5031a01525e0209a1581c34250edd1e9836f5378702fbf9416b709bc140e04f668cc355208518a1494154414441636f696e010758206cf243cc513691d9edc092b1030c6d1e5f9a8621a4d4383032b3d292d4679d5c81a200d90102828258201287e9ce9e00a603d250b557146aa0581fc4edf277a244ce39d3b2f2ced5072f5840d40fbe736892d8dab09e864a25f2e59fb7bfe445d960bbace30996965dc12a34c59746febf9d32ade65b6a9e1a1a6efc53830a3acaab699972cd4f240c024c0f825820742d8af3543349b5b18f3cba28f23b2d6e465b9c136c42e1fae6b2390f565427584005637b5645784bd998bb8ed837021d520200211fdd958b9a4d4b3af128fa6e695fb86abad7a9ddad6f1db946f8b812113fa16cfb7025e2397277b14e8c9bed0a01d90102818200581c45d70e54f3b5e9c5a2b0cd417028197bd6f5fa5378c2f5eba896678da100d90103a100a11902a2a1636d73678f78264175746f2d4c6f6f702d5472616e73616374696f6e202336323733363820627920415441444160783c4c6976652045706f6368203235352c207765206861766520303131682035396d20323573206c65667420756e74696c20746865206e657874206f6e6578344974277320536f6e6e746167202d20323520466562727561722032303234202d2031333a33303a333520696e20417573747269616060607820412072616e646f6d205a656e2d51756f746520666f7220796f753a20f09f998f78344974206973206e6576657220746f6f206c61746520746f206265207768617420796f75206d696768742068617665206265656e2e6f202d2047656f72676520456c696f746078374e6f64652d5265766973696f6e3a203462623230343864623737643632336565366533363738363138633264386236633436373633333360782953616e63686f4e657420697320617765736f6d652c206861766520736f6d652066756e2120f09f988d7819204265737420726567617264732c204d617274696e203a2d2980"; + + fn test_block_info(bytes: &[u8]) -> BlockInfo { + let block = MultiEraBlock::decode(bytes).unwrap(); + let genesis = GenesisValues::mainnet(); + let (epoch, epoch_slot) = block.epoch(&genesis); + let timestamp = block.wallclock(&genesis); + BlockInfo { + status: acropolis_common::BlockStatus::Immutable, + slot: block.slot(), + number: block.number(), + hash: block.hash().to_vec(), + epoch, + epoch_slot, + new_epoch: false, + timestamp, + era: acropolis_common::Era::Conway, + } + } - fn test_block() -> Block { - let tx = hex::decode(TEST_TX).unwrap(); + fn test_block_bytes() -> Vec { + hex::decode(TEST_BLOCK).unwrap() + } + + fn build_block(info: &BlockInfo, bytes: &[u8]) -> Block { + let extra = ExtraBlockData { + epoch: info.epoch, + epoch_slot: info.epoch_slot, + timestamp: info.timestamp, + }; Block { - number: 1, - slot: 3, - hash: vec![0xca, 0xfe, 0xd0, 0x0d], - txs: vec![tx], + bytes: bytes.to_vec(), + extra, } } @@ -220,31 +235,37 @@ mod tests { #[test] fn should_get_block_by_hash() { let state = init_state(); - let block = test_block(); + let bytes = test_block_bytes(); + let info = test_block_info(&bytes); + let block = build_block(&info, &bytes); - state.store.insert_block(&block).unwrap(); + state.store.insert_block(&info, &bytes).unwrap(); - let new_block = state.store.get_block_by_hash(&block.hash).unwrap(); + let new_block = state.store.get_block_by_hash(&info.hash).unwrap(); assert_eq!(block, new_block); } #[test] fn should_get_block_by_slot() { let state = init_state(); - let block = test_block(); + let bytes = test_block_bytes(); + let info = test_block_info(&bytes); + let block = build_block(&info, &bytes); - state.store.insert_block(&block).unwrap(); + state.store.insert_block(&info, &bytes).unwrap(); - let new_block = state.store.get_block_by_slot(block.slot).unwrap(); + let new_block = state.store.get_block_by_slot(info.slot).unwrap(); assert_eq!(block, new_block); } #[test] fn should_get_latest_block() { let state = init_state(); - let block = test_block(); + let bytes = test_block_bytes(); + let info = test_block_info(&bytes); + let block = build_block(&info, &bytes); - state.store.insert_block(&block).unwrap(); + state.store.insert_block(&info, &bytes).unwrap(); let new_block = state.store.get_latest_block().unwrap(); assert_eq!(block, new_block); diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 5f790677..43747810 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -4,32 +4,32 @@ use anyhow::{Context, Result}; pub mod fjall; pub trait Store: Send + Sync { - fn insert_block(&self, block: &Block) -> Result<()>; + fn insert_block(&self, info: &BlockInfo, block: &[u8]) -> Result<()>; fn get_block_by_hash(&self, hash: &[u8]) -> Result; fn get_block_by_slot(&self, slot: u64) -> Result; fn get_latest_block(&self) -> Result; } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, minicbor::Decode, minicbor::Encode)] pub struct Block { - slot: u64, - number: u64, - hash: Vec, - txs: Vec>, + #[n(0)] + pub bytes: Vec, + #[n(1)] + pub extra: ExtraBlockData, } -impl Block { - pub fn from_info_and_txs(info: &BlockInfo, txs: &[Vec]) -> Self { - Self { - slot: info.slot, - number: info.number, - hash: info.hash.clone(), - txs: txs.to_vec(), - } - } + +#[derive(Debug, PartialEq, Eq, minicbor::Decode, minicbor::Encode)] +pub struct ExtraBlockData { + #[n(0)] + pub epoch: u64, + #[n(1)] + pub epoch_slot: u64, + #[n(2)] + pub timestamp: u64, } -pub(crate) fn hash_tx(tx: &[u8]) -> Result { - let tx = pallas_traverse::MultiEraTx::decode(tx).context("could not decode tx")?; - Ok(TxHash::from(*tx.hash())) +pub(crate) fn extract_tx_hashes(block: &[u8]) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(block).context("could not decode block")?; + Ok(block.txs().into_iter().map(|tx| TxHash::from(*tx.hash())).collect()) } From ce3a99b81da6c573baf159a632c5f9a766454096 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 15:03:34 -0400 Subject: [PATCH 04/44] feat: add BlockInfo fields --- common/src/queries/blocks.rs | 22 +++++++++- modules/chain_store/src/chain_store.rs | 57 +++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 7c9b46ee..1cac2673 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,3 +1,5 @@ +use crate::KeyHash; + pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -34,7 +36,25 @@ pub enum BlocksStateQueryResponse { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockInfo {} +pub struct BlockInfo { + pub timestamp: u64, + pub number: u64, + pub hash: Vec, + pub slot: u64, + pub epoch: u64, + pub epoch_slot: u64, + pub issuer_vkey: Option>, + pub size: u64, + pub tx_count: u64, + pub output: Option, + pub fees: Option, + pub block_vrf: Option>, + pub op_cert: Option, + pub op_cert_counter: Option, + pub previous_block: Option>, + pub next_block: Option>, + pub confirmations: u64, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct NextBlocks {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 8df5c591..56b5d45d 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,6 +1,7 @@ mod stores; use acropolis_common::{ + crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ BlockInfo, BlocksStateQuery, BlocksStateQueryResponse, DEFAULT_BLOCKS_QUERY_TOPIC, @@ -84,24 +85,70 @@ impl ChainStore { match query { BlocksStateQuery::GetLatestBlock => { let block = store.get_latest_block()?; - let info = Self::to_block_info(block); + let info = Self::to_block_info(block)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } BlocksStateQuery::GetBlockInfo { block_key } => { let block = store.get_block_by_hash(block_key)?; - let info = Self::to_block_info(block); + let info = Self::to_block_info(block)?; Ok(BlocksStateQueryResponse::BlockInfo(info)) } BlocksStateQuery::GetBlockBySlot { slot } => { let block = store.get_block_by_slot(*slot)?; - let info = Self::to_block_info(block); + let info = Self::to_block_info(block)?; Ok(BlocksStateQueryResponse::BlockBySlot(info)) } other => bail!("{other:?} not yet supported"), } } - fn to_block_info(_block: Block) -> BlockInfo { - BlockInfo {} + fn to_block_info(block: Block) -> Result { + let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; + let header = decoded.header(); + let mut output = None; + let mut fees = None; + for tx in decoded.txs() { + if let Some(new_fee) = tx.fee() { + fees = Some(fees.unwrap_or_default() + new_fee); + } + for o in tx.outputs() { + output = Some(output.unwrap_or_default() + o.value().coin()) + } + } + let (op_cert_hot_vkey, op_cert_counter) = match &header { + pallas_traverse::MultiEraHeader::BabbageCompatible(h) => { + let cert = &h.header_body.operational_cert; + ( + Some(&cert.operational_cert_hot_vkey), + Some(cert.operational_cert_sequence_number), + ) + } + pallas_traverse::MultiEraHeader::ShelleyCompatible(h) => ( + Some(&h.header_body.operational_cert_hot_vkey), + Some(h.header_body.operational_cert_sequence_number), + ), + _ => (None, None), + }; + let op_cert = op_cert_hot_vkey.map(|vkey| keyhash(vkey)); + + Ok(BlockInfo { + timestamp: block.extra.timestamp, + number: header.number(), + hash: header.hash().to_vec(), + slot: header.slot(), + epoch: block.extra.epoch, + epoch_slot: block.extra.epoch_slot, + issuer_vkey: header.issuer_vkey().map(|key| key.to_vec()), + size: block.bytes.len() as u64, + tx_count: decoded.tx_count() as u64, + output, + fees, + block_vrf: header.vrf_vkey().map(|key| key.to_vec()), + op_cert, + op_cert_counter, + previous_block: header.previous_hash().map(|x| x.to_vec()), + next_block: None, // TODO + confirmations: 0, // TODO + }) } } From f895149801357ae3b41b6d4bf20d4e0bdc13c506 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 15:49:25 -0400 Subject: [PATCH 05/44] feat: populate remaining BlockInfo fields --- modules/chain_store/src/chain_store.rs | 60 ++++++++++++++++++++----- modules/chain_store/src/stores/fjall.rs | 44 ++++++++++++------ modules/chain_store/src/stores/mod.rs | 7 +-- 3 files changed, 83 insertions(+), 28 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 56b5d45d..92c8716c 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -84,25 +84,37 @@ impl ChainStore { ) -> Result { match query { BlocksStateQuery::GetLatestBlock => { - let block = store.get_latest_block()?; - let info = Self::to_block_info(block)?; - Ok(BlocksStateQueryResponse::LatestBlock(info)) + match store.get_latest_block()? { + Some(block) => { + let info = Self::to_block_info(block, store, true)?; + Ok(BlocksStateQueryResponse::LatestBlock(info)) + } + None => Ok(BlocksStateQueryResponse::NotFound) + } } BlocksStateQuery::GetBlockInfo { block_key } => { - let block = store.get_block_by_hash(block_key)?; - let info = Self::to_block_info(block)?; - Ok(BlocksStateQueryResponse::BlockInfo(info)) + match store.get_block_by_hash(&block_key)? { + Some(block) => { + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockInfo(info)) + } + None => Ok(BlocksStateQueryResponse::NotFound) + } } BlocksStateQuery::GetBlockBySlot { slot } => { - let block = store.get_block_by_slot(*slot)?; - let info = Self::to_block_info(block)?; - Ok(BlocksStateQueryResponse::BlockBySlot(info)) + match store.get_block_by_slot(*slot)? { + Some(block) => { + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockBySlot(info)) + } + None => Ok(BlocksStateQueryResponse::NotFound) + } } other => bail!("{other:?} not yet supported"), } } - fn to_block_info(block: Block) -> Result { + fn to_block_info(block: Block, store: &Arc, is_latest: bool) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; let header = decoded.header(); let mut output = None; @@ -131,6 +143,30 @@ impl ChainStore { }; let op_cert = op_cert_hot_vkey.map(|vkey| keyhash(vkey)); + let (next_block, confirmations) = if is_latest { + (None, 0) + } else { + let number = header.number(); + let raw_latest_block = store.get_latest_block()?.unwrap(); + let latest_block = pallas_traverse::MultiEraBlock::decode(&raw_latest_block.bytes)?; + let latest_block_number = latest_block.number(); + let confirmations = latest_block_number - number; + + let next_block_number = number + 1; + let next_block_hash = if next_block_number == latest_block_number { + Some(latest_block.hash().to_vec()) + } else { + let raw_next_block = store.get_block_by_number(next_block_number)?; + if let Some(raw_block) = raw_next_block { + let block = pallas_traverse::MultiEraBlock::decode(&raw_block.bytes)?; + Some(block.hash().to_vec()) + } else { + None + } + }; + (next_block_hash, confirmations) + }; + Ok(BlockInfo { timestamp: block.extra.timestamp, number: header.number(), @@ -147,8 +183,8 @@ impl ChainStore { op_cert, op_cert_counter, previous_block: header.previous_hash().map(|x| x.to_vec()), - next_block: None, // TODO - confirmations: 0, // TODO + next_block, + confirmations, }) } } diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index e4f9d80e..7b48961c 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,7 +1,7 @@ use std::{path::Path, sync::Arc}; use acropolis_common::{BlockInfo, TxHash}; -use anyhow::{bail, Result}; +use anyhow::Result; use config::Config; use fjall::{Batch, Keyspace, Partition}; @@ -62,15 +62,19 @@ impl super::Store for FjallStore { Ok(()) } - fn get_block_by_hash(&self, hash: &[u8]) -> Result { + fn get_block_by_hash(&self, hash: &[u8]) -> Result> { self.blocks.get_by_hash(hash) } - fn get_block_by_slot(&self, slot: u64) -> Result { + fn get_block_by_slot(&self, slot: u64) -> Result> { self.blocks.get_by_slot(slot) } - fn get_latest_block(&self) -> Result { + fn get_block_by_number(&self, number: u64) -> Result> { + self.blocks.get_by_number(number) + } + + fn get_latest_block(&self) -> Result> { self.blocks.get_latest() } } @@ -119,23 +123,30 @@ impl FjallBlockStore { ); } - fn get_by_hash(&self, hash: &[u8]) -> Result { + fn get_by_hash(&self, hash: &[u8]) -> Result> { let Some(block) = self.blocks.get(hash)? else { - bail!("No block found with hash {}", hex::encode(hash)); + return Ok(None); }; Ok(minicbor::decode(&block)?) } - fn get_by_slot(&self, slot: u64) -> Result { + fn get_by_slot(&self, slot: u64) -> Result> { let Some(hash) = self.block_hashes_by_slot.get(slot.to_be_bytes())? else { - bail!("No block found for slot {slot}"); + return Ok(None); + }; + self.get_by_hash(&hash) + } + + fn get_by_number(&self, number: u64) -> Result> { + let Some(hash) = self.block_hashes_by_number.get(number.to_be_bytes())? else { + return Ok(None); }; self.get_by_hash(&hash) } - fn get_latest(&self) -> Result { + fn get_latest(&self) -> Result> { let Some((_, hash)) = self.block_hashes_by_slot.last_key_value()? else { - bail!("No blocks found"); + return Ok(None); }; self.get_by_hash(&hash) } @@ -242,7 +253,14 @@ mod tests { state.store.insert_block(&info, &bytes).unwrap(); let new_block = state.store.get_block_by_hash(&info.hash).unwrap(); - assert_eq!(block, new_block); + assert_eq!(block, new_block.unwrap()); + } + + #[test] + fn should_not_error_when_block_not_found() { + let state = init_state(); + let new_block = state.store.get_block_by_hash(&[0xfa, 0x15, 0xe]).unwrap(); + assert_eq!(new_block, None); } #[test] @@ -255,7 +273,7 @@ mod tests { state.store.insert_block(&info, &bytes).unwrap(); let new_block = state.store.get_block_by_slot(info.slot).unwrap(); - assert_eq!(block, new_block); + assert_eq!(block, new_block.unwrap()); } #[test] @@ -268,6 +286,6 @@ mod tests { state.store.insert_block(&info, &bytes).unwrap(); let new_block = state.store.get_latest_block().unwrap(); - assert_eq!(block, new_block); + assert_eq!(block, new_block.unwrap()); } } diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 43747810..49bb2129 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -6,9 +6,10 @@ pub mod fjall; pub trait Store: Send + Sync { fn insert_block(&self, info: &BlockInfo, block: &[u8]) -> Result<()>; - fn get_block_by_hash(&self, hash: &[u8]) -> Result; - fn get_block_by_slot(&self, slot: u64) -> Result; - fn get_latest_block(&self) -> Result; + fn get_block_by_hash(&self, hash: &[u8]) -> Result>; + fn get_block_by_slot(&self, slot: u64) -> Result>; + fn get_block_by_number(&self, number: u64) -> Result>; + fn get_latest_block(&self) -> Result>; } #[derive(Debug, PartialEq, Eq, minicbor::Decode, minicbor::Encode)] From 62cba5580ce513a534895f3f328e41aa543e1ed8 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 16:00:08 -0400 Subject: [PATCH 06/44] feat: support getting blocks by epoch+slot --- modules/chain_store/src/chain_store.rs | 33 ++++++++------ modules/chain_store/src/stores/fjall.rs | 57 +++++++++++++++++++++++++ modules/chain_store/src/stores/mod.rs | 1 + 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 92c8716c..587cd30f 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -83,31 +83,36 @@ impl ChainStore { query: &BlocksStateQuery, ) -> Result { match query { - BlocksStateQuery::GetLatestBlock => { - match store.get_latest_block()? { - Some(block) => { - let info = Self::to_block_info(block, store, true)?; - Ok(BlocksStateQueryResponse::LatestBlock(info)) - } - None => Ok(BlocksStateQueryResponse::NotFound) + BlocksStateQuery::GetLatestBlock => match store.get_latest_block()? { + Some(block) => { + let info = Self::to_block_info(block, store, true)?; + Ok(BlocksStateQueryResponse::LatestBlock(info)) } - } + None => Ok(BlocksStateQueryResponse::NotFound), + }, BlocksStateQuery::GetBlockInfo { block_key } => { - match store.get_block_by_hash(&block_key)? { + match store.get_block_by_hash(block_key)? { Some(block) => { let info = Self::to_block_info(block, store, false)?; Ok(BlocksStateQueryResponse::BlockInfo(info)) } - None => Ok(BlocksStateQueryResponse::NotFound) + None => Ok(BlocksStateQueryResponse::NotFound), } } - BlocksStateQuery::GetBlockBySlot { slot } => { - match store.get_block_by_slot(*slot)? { + BlocksStateQuery::GetBlockBySlot { slot } => match store.get_block_by_slot(*slot)? { + Some(block) => { + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockBySlot(info)) + } + None => Ok(BlocksStateQueryResponse::NotFound), + }, + BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { + match store.get_block_by_epoch_slot(*epoch, *slot)? { Some(block) => { let info = Self::to_block_info(block, store, false)?; - Ok(BlocksStateQueryResponse::BlockBySlot(info)) + Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) } - None => Ok(BlocksStateQueryResponse::NotFound) + None => Ok(BlocksStateQueryResponse::NotFound), } } other => bail!("{other:?} not yet supported"), diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index 7b48961c..ecd59461 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -17,6 +17,7 @@ const DEFAULT_DATABASE_PATH: &str = "fjall-blocks"; const BLOCKS_PARTITION: &str = "blocks"; const BLOCK_HASHES_BY_SLOT_PARTITION: &str = "block-hashes-by-slot"; const BLOCK_HASHES_BY_NUMBER_PARTITION: &str = "block-hashes-by-number"; +const BLOCK_HASHES_BY_EPOCH_SLOT_PARTITION: &str = "block-hashes-by-epoch-slot"; const TXS_PARTITION: &str = "txs"; impl FjallStore { @@ -74,6 +75,10 @@ impl super::Store for FjallStore { self.blocks.get_by_number(number) } + fn get_block_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result> { + self.blocks.get_by_epoch_slot(epoch, epoch_slot) + } + fn get_latest_block(&self) -> Result> { self.blocks.get_latest() } @@ -83,6 +88,7 @@ struct FjallBlockStore { blocks: Partition, block_hashes_by_slot: Partition, block_hashes_by_number: Partition, + block_hashes_by_epoch_slot: Partition, } impl FjallBlockStore { @@ -97,10 +103,15 @@ impl FjallBlockStore { BLOCK_HASHES_BY_NUMBER_PARTITION, fjall::PartitionCreateOptions::default(), )?; + let block_hashes_by_epoch_slot = keyspace.open_partition( + BLOCK_HASHES_BY_EPOCH_SLOT_PARTITION, + fjall::PartitionCreateOptions::default(), + )?; Ok(Self { blocks, block_hashes_by_slot, block_hashes_by_number, + block_hashes_by_epoch_slot, }) } @@ -121,6 +132,11 @@ impl FjallBlockStore { info.number.to_be_bytes(), &info.hash, ); + batch.insert( + &self.block_hashes_by_epoch_slot, + epoch_slot_key(info.epoch, info.epoch_slot), + &info.hash, + ); } fn get_by_hash(&self, hash: &[u8]) -> Result> { @@ -144,6 +160,14 @@ impl FjallBlockStore { self.get_by_hash(&hash) } + fn get_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result> { + let Some(hash) = self.block_hashes_by_epoch_slot.get(epoch_slot_key(epoch, epoch_slot))? + else { + return Ok(None); + }; + self.get_by_hash(&hash) + } + fn get_latest(&self) -> Result> { let Some((_, hash)) = self.block_hashes_by_slot.last_key_value()? else { return Ok(None); @@ -152,6 +176,13 @@ impl FjallBlockStore { } } +fn epoch_slot_key(epoch: u64, epoch_slot: u64) -> [u8; 16] { + let mut key = [0; 16]; + key[..8].copy_from_slice(epoch.to_be_bytes().as_slice()); + key[8..].copy_from_slice(epoch_slot.to_be_bytes().as_slice()); + key +} + struct FjallTXStore { txs: Partition, } @@ -276,6 +307,32 @@ mod tests { assert_eq!(block, new_block.unwrap()); } + #[test] + fn should_get_block_by_number() { + let state = init_state(); + let bytes = test_block_bytes(); + let info = test_block_info(&bytes); + let block = build_block(&info, &bytes); + + state.store.insert_block(&info, &bytes).unwrap(); + + let new_block = state.store.get_block_by_number(info.number).unwrap(); + assert_eq!(block, new_block.unwrap()); + } + + #[test] + fn should_get_block_by_epoch_slot() { + let state = init_state(); + let bytes = test_block_bytes(); + let info = test_block_info(&bytes); + let block = build_block(&info, &bytes); + + state.store.insert_block(&info, &bytes).unwrap(); + + let new_block = state.store.get_block_by_epoch_slot(info.epoch, info.epoch_slot).unwrap(); + assert_eq!(block, new_block.unwrap()); + } + #[test] fn should_get_latest_block() { let state = init_state(); diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 49bb2129..0c0ad741 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -9,6 +9,7 @@ pub trait Store: Send + Sync { fn get_block_by_hash(&self, hash: &[u8]) -> Result>; fn get_block_by_slot(&self, slot: u64) -> Result>; fn get_block_by_number(&self, number: u64) -> Result>; + fn get_block_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result>; fn get_latest_block(&self) -> Result>; } From 704ad6a90f8f2ec954a7a644b770e9d640a03dcf Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 17:42:35 -0400 Subject: [PATCH 07/44] feat: support getting next and previous blocks --- common/src/queries/blocks.rs | 45 +++-- modules/chain_store/src/chain_store.rs | 211 +++++++++++++++++------- modules/chain_store/src/stores/fjall.rs | 22 +++ modules/chain_store/src/stores/mod.rs | 1 + 4 files changed, 207 insertions(+), 72 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 1cac2673..3fd1149e 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -8,14 +8,35 @@ pub enum BlocksStateQuery { GetLatestBlock, GetLatestBlockTransactions, GetLatestBlockTransactionsCBOR, - GetBlockInfo { block_key: Vec }, - GetNextBlocks { block_key: Vec }, - GetPreviousBlocks { block_key: Vec }, - GetBlockBySlot { slot: u64 }, - GetBlockByEpochSlot { epoch: u64, slot: u64 }, - GetBlockTransactions { block_key: Vec }, - GetBlockTransactionsCBOR { block_key: Vec }, - GetBlockInvolvedAddresses { block_key: Vec }, + GetBlockInfo { + block_key: Vec, + }, + GetNextBlocks { + block_key: Vec, + limit: u64, + skip: u64, + }, + GetPreviousBlocks { + block_key: Vec, + limit: u64, + skip: u64, + }, + GetBlockBySlot { + slot: u64, + }, + GetBlockByEpochSlot { + epoch: u64, + slot: u64, + }, + GetBlockTransactions { + block_key: Vec, + }, + GetBlockTransactionsCBOR { + block_key: Vec, + }, + GetBlockInvolvedAddresses { + block_key: Vec, + }, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -57,10 +78,14 @@ pub struct BlockInfo { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct NextBlocks {} +pub struct NextBlocks { + pub blocks: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PreviousBlocks {} +pub struct PreviousBlocks { + pub blocks: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BlockTransactions {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 587cd30f..48c8d4d0 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -4,7 +4,8 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlocksStateQuery, BlocksStateQueryResponse, DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, + DEFAULT_BLOCKS_QUERY_TOPIC, }, }; use anyhow::{bail, Result}; @@ -115,81 +116,167 @@ impl ChainStore { None => Ok(BlocksStateQueryResponse::NotFound), } } + BlocksStateQuery::GetNextBlocks { + block_key, + limit, + skip, + } => { + if *limit == 0 { + return Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { + blocks: vec![], + })); + } + match store.get_block_by_hash(&block_key)? { + Some(block) => { + let number = Self::get_block_number(&block)?; + let min_number = number + 1 + skip; + let max_number = min_number + limit - 1; + let blocks = store.get_blocks_by_number_range(min_number, max_number)?; + let info = Self::to_block_info_bulk(blocks, store, false)?; + Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { + blocks: info, + })) + } + None => Ok(BlocksStateQueryResponse::NotFound), + } + } + BlocksStateQuery::GetPreviousBlocks { + block_key, + limit, + skip, + } => { + if *limit == 0 { + return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { + blocks: vec![], + })); + } + match store.get_block_by_hash(&block_key)? { + Some(block) => { + let number = Self::get_block_number(&block)?; + let Some(max_number) = number.checked_sub(1 + skip) else { + return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { + blocks: vec![], + })); + }; + let min_number = max_number.saturating_sub(limit - 1); + let blocks = store.get_blocks_by_number_range(min_number, max_number)?; + let info = Self::to_block_info_bulk(blocks, store, false)?; + Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { + blocks: info, + })) + } + None => Ok(BlocksStateQueryResponse::NotFound), + } + } + other => bail!("{other:?} not yet supported"), } } + fn get_block_number(block: &Block) -> Result { + Ok(pallas_traverse::MultiEraBlock::decode(&block.bytes)?.number()) + } + fn to_block_info(block: Block, store: &Arc, is_latest: bool) -> Result { - let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; - let header = decoded.header(); - let mut output = None; - let mut fees = None; - for tx in decoded.txs() { - if let Some(new_fee) = tx.fee() { - fees = Some(fees.unwrap_or_default() + new_fee); - } - for o in tx.outputs() { - output = Some(output.unwrap_or_default() + o.value().coin()) - } + let blocks = vec![block]; + let mut info = Self::to_block_info_bulk(blocks, store, is_latest)?; + Ok(info.remove(0)) + } + + fn to_block_info_bulk( + blocks: Vec, + store: &Arc, + final_block_is_latest: bool, + ) -> Result> { + if blocks.is_empty() { + return Ok(vec![]); } - let (op_cert_hot_vkey, op_cert_counter) = match &header { - pallas_traverse::MultiEraHeader::BabbageCompatible(h) => { - let cert = &h.header_body.operational_cert; - ( - Some(&cert.operational_cert_hot_vkey), - Some(cert.operational_cert_sequence_number), - ) - } - pallas_traverse::MultiEraHeader::ShelleyCompatible(h) => ( - Some(&h.header_body.operational_cert_hot_vkey), - Some(h.header_body.operational_cert_sequence_number), - ), - _ => (None, None), + let mut decoded_blocks = vec![]; + for block in &blocks { + decoded_blocks.push(pallas_traverse::MultiEraBlock::decode(&block.bytes)?); + } + + let (latest_number, latest_hash) = if final_block_is_latest { + let latest = decoded_blocks.last().unwrap(); + (latest.number(), latest.hash()) + } else { + let raw_latest = store.get_latest_block()?.unwrap(); + let latest = pallas_traverse::MultiEraBlock::decode(&raw_latest.bytes)?; + (latest.number(), latest.hash()) }; - let op_cert = op_cert_hot_vkey.map(|vkey| keyhash(vkey)); - let (next_block, confirmations) = if is_latest { - (None, 0) + let mut next_hash = if final_block_is_latest { + None } else { - let number = header.number(); - let raw_latest_block = store.get_latest_block()?.unwrap(); - let latest_block = pallas_traverse::MultiEraBlock::decode(&raw_latest_block.bytes)?; - let latest_block_number = latest_block.number(); - let confirmations = latest_block_number - number; - - let next_block_number = number + 1; - let next_block_hash = if next_block_number == latest_block_number { - Some(latest_block.hash().to_vec()) + let next_number = decoded_blocks.last().unwrap().number() + 1; + if next_number > latest_number { + None + } else if next_number == latest_number { + Some(latest_hash) } else { - let raw_next_block = store.get_block_by_number(next_block_number)?; - if let Some(raw_block) = raw_next_block { - let block = pallas_traverse::MultiEraBlock::decode(&raw_block.bytes)?; - Some(block.hash().to_vec()) + let raw_next = store.get_block_by_number(next_number)?; + if let Some(raw_next) = raw_next { + let next = pallas_traverse::MultiEraBlock::decode(&raw_next.bytes)?; + Some(next.hash()) } else { None } - }; - (next_block_hash, confirmations) + } }; - Ok(BlockInfo { - timestamp: block.extra.timestamp, - number: header.number(), - hash: header.hash().to_vec(), - slot: header.slot(), - epoch: block.extra.epoch, - epoch_slot: block.extra.epoch_slot, - issuer_vkey: header.issuer_vkey().map(|key| key.to_vec()), - size: block.bytes.len() as u64, - tx_count: decoded.tx_count() as u64, - output, - fees, - block_vrf: header.vrf_vkey().map(|key| key.to_vec()), - op_cert, - op_cert_counter, - previous_block: header.previous_hash().map(|x| x.to_vec()), - next_block, - confirmations, - }) + let mut block_info = vec![]; + for (block, decoded) in blocks.iter().zip(decoded_blocks).rev() { + let header = decoded.header(); + let mut output = None; + let mut fees = None; + for tx in decoded.txs() { + if let Some(new_fee) = tx.fee() { + fees = Some(fees.unwrap_or_default() + new_fee); + } + for o in tx.outputs() { + output = Some(output.unwrap_or_default() + o.value().coin()) + } + } + let (op_cert_hot_vkey, op_cert_counter) = match &header { + pallas_traverse::MultiEraHeader::BabbageCompatible(h) => { + let cert = &h.header_body.operational_cert; + ( + Some(&cert.operational_cert_hot_vkey), + Some(cert.operational_cert_sequence_number), + ) + } + pallas_traverse::MultiEraHeader::ShelleyCompatible(h) => ( + Some(&h.header_body.operational_cert_hot_vkey), + Some(h.header_body.operational_cert_sequence_number), + ), + _ => (None, None), + }; + let op_cert = op_cert_hot_vkey.map(|vkey| keyhash(vkey)); + + block_info.push(BlockInfo { + timestamp: block.extra.timestamp, + number: header.number(), + hash: header.hash().to_vec(), + slot: header.slot(), + epoch: block.extra.epoch, + epoch_slot: block.extra.epoch_slot, + issuer_vkey: header.issuer_vkey().map(|key| key.to_vec()), + size: block.bytes.len() as u64, + tx_count: decoded.tx_count() as u64, + output, + fees, + block_vrf: header.vrf_vkey().map(|key| key.to_vec()), + op_cert, + op_cert_counter, + previous_block: header.previous_hash().map(|h| h.to_vec()), + next_block: next_hash.map(|h| h.to_vec()), + confirmations: latest_number - header.number(), + }); + + next_hash = Some(header.hash()); + } + + block_info.reverse(); + Ok(block_info) } } diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index ecd59461..1161c908 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -75,6 +75,10 @@ impl super::Store for FjallStore { self.blocks.get_by_number(number) } + fn get_blocks_by_number_range(&self, min_number: u64, max_number: u64) -> Result> { + self.blocks.get_by_number_range(min_number, max_number) + } + fn get_block_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result> { self.blocks.get_by_epoch_slot(epoch, epoch_slot) } @@ -160,6 +164,24 @@ impl FjallBlockStore { self.get_by_hash(&hash) } + fn get_by_number_range(&self, min_number: u64, max_number: u64) -> Result> { + let min_number_bytes = min_number.to_be_bytes(); + let max_number_bytes = max_number.to_be_bytes(); + let mut hashes = vec![]; + for res in self.block_hashes_by_number.range(min_number_bytes..=max_number_bytes) { + let (_, hash) = res?; + hashes.push(hash); + } + + let mut blocks = vec![]; + for hash in hashes { + if let Some(block) = self.get_by_hash(&hash)? { + blocks.push(block); + } + } + Ok(blocks) + } + fn get_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result> { let Some(hash) = self.block_hashes_by_epoch_slot.get(epoch_slot_key(epoch, epoch_slot))? else { diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 0c0ad741..4be55815 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -9,6 +9,7 @@ pub trait Store: Send + Sync { fn get_block_by_hash(&self, hash: &[u8]) -> Result>; fn get_block_by_slot(&self, slot: u64) -> Result>; fn get_block_by_number(&self, number: u64) -> Result>; + fn get_blocks_by_number_range(&self, min_number: u64, max_number: u64) -> Result>; fn get_block_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result>; fn get_latest_block(&self) -> Result>; } From 1e9f1a1c0547f186331b99b21389ac4ef8183f15 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 17:44:59 -0400 Subject: [PATCH 08/44] feat: return transactions and transactions cbor --- common/src/queries/blocks.rs | 16 ++- modules/chain_store/src/chain_store.rs | 161 ++++++++++++++++--------- 2 files changed, 114 insertions(+), 63 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 3fd1149e..b69039c7 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,4 +1,4 @@ -use crate::KeyHash; +use crate::{KeyHash, TxHash}; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -88,10 +88,20 @@ pub struct PreviousBlocks { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockTransactions {} +pub struct BlockTransactions { + pub hashes: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockTransactionsCBOR {} +pub struct BlockTransactionsCBOR { + pub txs: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BlockTransaction { + pub hash: TxHash, + pub cbor: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BlockInvolvedAddresses {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 48c8d4d0..ad11705f 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -4,9 +4,10 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, - DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, + BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, + TxHash, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -84,37 +85,47 @@ impl ChainStore { query: &BlocksStateQuery, ) -> Result { match query { - BlocksStateQuery::GetLatestBlock => match store.get_latest_block()? { - Some(block) => { - let info = Self::to_block_info(block, store, true)?; - Ok(BlocksStateQueryResponse::LatestBlock(info)) - } - None => Ok(BlocksStateQueryResponse::NotFound), - }, + BlocksStateQuery::GetLatestBlock => { + let Some(block) = store.get_latest_block()? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let info = Self::to_block_info(block, store, true)?; + Ok(BlocksStateQueryResponse::LatestBlock(info)) + } + BlocksStateQuery::GetLatestBlockTransactions => { + let Some(block) = store.get_latest_block()? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let txs = Self::to_block_transactions(block)?; + Ok(BlocksStateQueryResponse::LatestBlockTransactions(txs)) + } + BlocksStateQuery::GetLatestBlockTransactionsCBOR => { + let Some(block) = store.get_latest_block()? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let txs = Self::to_block_transactions_cbor(block)?; + Ok(BlocksStateQueryResponse::LatestBlockTransactionsCBOR(txs)) + } BlocksStateQuery::GetBlockInfo { block_key } => { - match store.get_block_by_hash(block_key)? { - Some(block) => { - let info = Self::to_block_info(block, store, false)?; - Ok(BlocksStateQueryResponse::BlockInfo(info)) - } - None => Ok(BlocksStateQueryResponse::NotFound), - } + let Some(block) = store.get_block_by_hash(block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockInfo(info)) + } + BlocksStateQuery::GetBlockBySlot { slot } => { + let Some(block) = store.get_block_by_slot(*slot)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockBySlot(info)) } - BlocksStateQuery::GetBlockBySlot { slot } => match store.get_block_by_slot(*slot)? { - Some(block) => { - let info = Self::to_block_info(block, store, false)?; - Ok(BlocksStateQueryResponse::BlockBySlot(info)) - } - None => Ok(BlocksStateQueryResponse::NotFound), - }, BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { - match store.get_block_by_epoch_slot(*epoch, *slot)? { - Some(block) => { - let info = Self::to_block_info(block, store, false)?; - Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) - } - None => Ok(BlocksStateQueryResponse::NotFound), - } + let Some(block) = store.get_block_by_epoch_slot(*epoch, *slot)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let info = Self::to_block_info(block, store, false)?; + Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) } BlocksStateQuery::GetNextBlocks { block_key, @@ -126,19 +137,17 @@ impl ChainStore { blocks: vec![], })); } - match store.get_block_by_hash(&block_key)? { - Some(block) => { - let number = Self::get_block_number(&block)?; - let min_number = number + 1 + skip; - let max_number = min_number + limit - 1; - let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, false)?; - Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { - blocks: info, - })) - } - None => Ok(BlocksStateQueryResponse::NotFound), - } + let Some(block) = store.get_block_by_hash(&block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let number = Self::get_block_number(&block)?; + let min_number = number + 1 + skip; + let max_number = min_number + limit - 1; + let blocks = store.get_blocks_by_number_range(min_number, max_number)?; + let info = Self::to_block_info_bulk(blocks, store, false)?; + Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { + blocks: info, + })) } BlocksStateQuery::GetPreviousBlocks { block_key, @@ -150,23 +159,35 @@ impl ChainStore { blocks: vec![], })); } - match store.get_block_by_hash(&block_key)? { - Some(block) => { - let number = Self::get_block_number(&block)?; - let Some(max_number) = number.checked_sub(1 + skip) else { - return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { - blocks: vec![], - })); - }; - let min_number = max_number.saturating_sub(limit - 1); - let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, false)?; - Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { - blocks: info, - })) - } - None => Ok(BlocksStateQueryResponse::NotFound), - } + let Some(block) = store.get_block_by_hash(&block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let number = Self::get_block_number(&block)?; + let Some(max_number) = number.checked_sub(1 + skip) else { + return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { + blocks: vec![], + })); + }; + let min_number = max_number.saturating_sub(limit - 1); + let blocks = store.get_blocks_by_number_range(min_number, max_number)?; + let info = Self::to_block_info_bulk(blocks, store, false)?; + Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { + blocks: info, + })) + } + BlocksStateQuery::GetBlockTransactions { block_key } => { + let Some(block) = store.get_block_by_hash(block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let txs = Self::to_block_transactions(block)?; + Ok(BlocksStateQueryResponse::BlockTransactions(txs)) + } + BlocksStateQuery::GetBlockTransactionsCBOR { block_key } => { + let Some(block) = store.get_block_by_hash(block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let txs = Self::to_block_transactions_cbor(block)?; + Ok(BlocksStateQueryResponse::BlockTransactionsCBOR(txs)) } other => bail!("{other:?} not yet supported"), @@ -279,4 +300,24 @@ impl ChainStore { block_info.reverse(); Ok(block_info) } + + fn to_block_transactions(block: Block) -> Result { + let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; + let hashes = decoded.txs().iter().map(|tx| TxHash::from(*tx.hash())).collect(); + Ok(BlockTransactions { hashes }) + } + + fn to_block_transactions_cbor(block: Block) -> Result { + let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; + let txs = decoded + .txs() + .iter() + .map(|tx| { + let hash = TxHash::from(*tx.hash()); + let cbor = tx.encode(); + BlockTransaction { hash, cbor } + }) + .collect(); + Ok(BlockTransactionsCBOR { txs }) + } } From 2c21d6b67cf9f264e68e87081d57d5fcd88d062b Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 5 Sep 2025 17:53:24 -0400 Subject: [PATCH 09/44] feat: accept hash or number in state query --- common/src/queries/blocks.rs | 18 ++++++++++++------ modules/chain_store/src/chain_store.rs | 22 +++++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index b69039c7..72871820 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -9,15 +9,15 @@ pub enum BlocksStateQuery { GetLatestBlockTransactions, GetLatestBlockTransactionsCBOR, GetBlockInfo { - block_key: Vec, + block_key: BlockKey, }, GetNextBlocks { - block_key: Vec, + block_key: BlockKey, limit: u64, skip: u64, }, GetPreviousBlocks { - block_key: Vec, + block_key: BlockKey, limit: u64, skip: u64, }, @@ -29,16 +29,22 @@ pub enum BlocksStateQuery { slot: u64, }, GetBlockTransactions { - block_key: Vec, + block_key: BlockKey, }, GetBlockTransactionsCBOR { - block_key: Vec, + block_key: BlockKey, }, GetBlockInvolvedAddresses { - block_key: Vec, + block_key: BlockKey, }, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum BlockKey { + Hash(Vec), + Number(u64), +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlocksStateQueryResponse { LatestBlock(BlockInfo), diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index ad11705f..0eefee25 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -4,8 +4,9 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, - BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlockKey, BlockTransaction, BlockTransactions, BlockTransactionsCBOR, + BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, + DEFAULT_BLOCKS_QUERY_TOPIC, }, TxHash, }; @@ -107,7 +108,7 @@ impl ChainStore { Ok(BlocksStateQueryResponse::LatestBlockTransactionsCBOR(txs)) } BlocksStateQuery::GetBlockInfo { block_key } => { - let Some(block) = store.get_block_by_hash(block_key)? else { + let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let info = Self::to_block_info(block, store, false)?; @@ -137,7 +138,7 @@ impl ChainStore { blocks: vec![], })); } - let Some(block) = store.get_block_by_hash(&block_key)? else { + let Some(block) = Self::get_block_by_key(store, &block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let number = Self::get_block_number(&block)?; @@ -159,7 +160,7 @@ impl ChainStore { blocks: vec![], })); } - let Some(block) = store.get_block_by_hash(&block_key)? else { + let Some(block) = Self::get_block_by_key(store, &block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let number = Self::get_block_number(&block)?; @@ -176,14 +177,14 @@ impl ChainStore { })) } BlocksStateQuery::GetBlockTransactions { block_key } => { - let Some(block) = store.get_block_by_hash(block_key)? else { + let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let txs = Self::to_block_transactions(block)?; Ok(BlocksStateQueryResponse::BlockTransactions(txs)) } BlocksStateQuery::GetBlockTransactionsCBOR { block_key } => { - let Some(block) = store.get_block_by_hash(block_key)? else { + let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let txs = Self::to_block_transactions_cbor(block)?; @@ -194,6 +195,13 @@ impl ChainStore { } } + fn get_block_by_key(store: &Arc, block_key: &BlockKey) -> Result> { + match block_key { + BlockKey::Hash(hash) => store.get_block_by_hash(hash), + BlockKey::Number(number) => store.get_block_by_number(*number), + } + } + fn get_block_number(block: &Block) -> Result { Ok(pallas_traverse::MultiEraBlock::decode(&block.bytes)?.number()) } From f0183fe2debe4208d53cd5d8f4077df8dec10350 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 10 Sep 2025 17:57:40 +0100 Subject: [PATCH 10/44] Add blocks/latest blockfrost REST endpoint - Doesn't serialise byte arrays correctly yet --- Cargo.lock | 1 + .../rest_blockfrost/src/handlers/blocks.rs | 55 +++++++++++++++++++ modules/rest_blockfrost/src/handlers/mod.rs | 1 + .../rest_blockfrost/src/handlers_config.rs | 7 +++ .../rest_blockfrost/src/rest_blockfrost.rs | 15 +++++ processes/omnibus/Cargo.toml | 1 + processes/omnibus/omnibus.toml | 2 + processes/omnibus/src/main.rs | 2 + 8 files changed, 84 insertions(+) create mode 100644 modules/rest_blockfrost/src/handlers/blocks.rs diff --git a/Cargo.lock b/Cargo.lock index 47619688..e421b439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,7 @@ dependencies = [ "acropolis_common", "acropolis_module_accounts_state", "acropolis_module_block_unpacker", + "acropolis_module_chain_store", "acropolis_module_drdd_state", "acropolis_module_drep_state", "acropolis_module_epoch_activity_counter", diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs new file mode 100644 index 00000000..c44303cf --- /dev/null +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -0,0 +1,55 @@ +//! REST handlers for Acropolis Blockfrost /blocks endpoints +use acropolis_common::{ + messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, + queries::{ + blocks::{BlocksStateQuery, BlocksStateQueryResponse}, + utils::query_state, + }, +}; +use anyhow::Result; +use caryatid_sdk::Context; +use std::sync::Arc; + +use crate::handlers_config::HandlersConfig; + +/// Handle `/blocks/latest` +pub async fn handle_blocks_latest_blockfrost( + context: Arc>, + _: Vec, + handlers_config: Arc, +) -> Result { + let blocks_latest_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetLatestBlock, + ))); + let block_info = query_state( + &context, + &handlers_config.blocks_query_topic, + blocks_latest_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::LatestBlock(blocks_latest), + )) => Ok(blocks_latest), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving latest block: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving latest block" + )) + } + }, + ) + .await?; + + match serde_json::to_string(&block_info) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block info: {e}"), + )), + } +} diff --git a/modules/rest_blockfrost/src/handlers/mod.rs b/modules/rest_blockfrost/src/handlers/mod.rs index 1ef12115..1a3aacf6 100644 --- a/modules/rest_blockfrost/src/handlers/mod.rs +++ b/modules/rest_blockfrost/src/handlers/mod.rs @@ -1,4 +1,5 @@ pub mod accounts; +pub mod blocks; pub mod epochs; pub mod governance; pub mod pools; diff --git a/modules/rest_blockfrost/src/handlers_config.rs b/modules/rest_blockfrost/src/handlers_config.rs index f7863056..3b3dea26 100644 --- a/modules/rest_blockfrost/src/handlers_config.rs +++ b/modules/rest_blockfrost/src/handlers_config.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use acropolis_common::queries::{ accounts::DEFAULT_ACCOUNTS_QUERY_TOPIC, + blocks::DEFAULT_BLOCKS_QUERY_TOPIC, epochs::DEFAULT_EPOCHS_QUERY_TOPIC, governance::{DEFAULT_DREPS_QUERY_TOPIC, DEFAULT_GOVERNANCE_QUERY_TOPIC}, parameters::DEFAULT_PARAMETERS_QUERY_TOPIC, @@ -14,6 +15,7 @@ const DEFAULT_EXTERNAL_API_TIMEOUT: (&str, i64) = ("external_api_timeout", 3); / #[derive(Clone)] pub struct HandlersConfig { pub accounts_query_topic: String, + pub blocks_query_topic: String, pub pools_query_topic: String, pub dreps_query_topic: String, pub governance_query_topic: String, @@ -28,6 +30,10 @@ impl From> for HandlersConfig { .get_string(DEFAULT_ACCOUNTS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_ACCOUNTS_QUERY_TOPIC.1.to_string()); + let blocks_query_topic = config + .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) + .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); + let pools_query_topic = config .get_string(DEFAULT_POOLS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_POOLS_QUERY_TOPIC.1.to_string()); @@ -54,6 +60,7 @@ impl From> for HandlersConfig { Self { accounts_query_topic, + blocks_query_topic, pools_query_topic, dreps_query_topic, governance_query_topic, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 114cede6..a4853b4b 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -17,6 +17,9 @@ mod types; mod utils; use handlers::{ accounts::handle_single_account_blockfrost, + blocks::{ + handle_blocks_latest_blockfrost, + }, epochs::{ handle_epoch_info_blockfrost, handle_epoch_next_blockfrost, handle_epoch_params_blockfrost, handle_epoch_pool_blocks_blockfrost, handle_epoch_pool_stakes_blockfrost, @@ -46,6 +49,10 @@ use crate::handlers_config::HandlersConfig; const DEFAULT_HANDLE_SINGLE_ACCOUNT_TOPIC: (&str, &str) = ("handle-topic-account-single", "rest.get.accounts.*"); +// Blocks topics +const DEFAULT_HANDLE_BLOCKS_LATEST_TOPIC: (&str, &str) = + ("handle-blocks-latest", "rest.get.blocks.latest"); + // Governance topics const DEFAULT_HANDLE_DREPS_LIST_TOPIC: (&str, &str) = ("handle-topic-dreps-list", "rest.get.governance.dreps"); @@ -165,6 +172,14 @@ impl BlockfrostREST { handle_single_account_blockfrost, ); + // Handler for /blocks/latest + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_LATEST_TOPIC, + handlers_config.clone(), + handle_blocks_latest_blockfrost, + ); + // Handler for /governance/dreps register_handler( context.clone(), diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 957bdcdf..f1fadb19 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -34,6 +34,7 @@ acropolis_module_accounts_state = { path = "../../modules/accounts_state" } acropolis_module_rest_blockfrost = { path = "../../modules/rest_blockfrost" } acropolis_module_spdd_state = { path = "../../modules/spdd_state" } acropolis_module_drdd_state = { path = "../../modules/drdd_state" } +acropolis_module_chain_store = { path = "../../modules/chain_store" } anyhow = "1.0" config = "0.15.11" diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index c2cadd55..0ceed98c 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -71,6 +71,8 @@ store-history = false [module.accounts-state] +[module.chain-store] + [module.clock] [module.rest-server] diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 9007ed63..e013f4f2 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -11,6 +11,7 @@ use tracing_subscriber; // External modules use acropolis_module_accounts_state::AccountsState; use acropolis_module_block_unpacker::BlockUnpacker; +use acropolis_module_chain_store::ChainStore; use acropolis_module_drdd_state::DRDDState; use acropolis_module_drep_state::DRepState; use acropolis_module_epoch_activity_counter::EpochActivityCounter; @@ -101,6 +102,7 @@ pub async fn main() -> Result<()> { BlockfrostREST::register(&mut process); SPDDState::register(&mut process); DRDDState::register(&mut process); + ChainStore::register(&mut process); Clock::::register(&mut process); RESTServer::::register(&mut process); From 642e0fa13a48c50c6f7e7fc41f347862465419c1 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 12 Sep 2025 18:28:34 +0100 Subject: [PATCH 11/44] Implement some serialization for BlockInfo --- Cargo.lock | 17 ++++--- common/Cargo.toml | 1 + common/src/queries/blocks.rs | 45 ++++++++++++++++--- common/src/types.rs | 8 ++++ modules/chain_store/src/chain_store.rs | 7 +-- .../rest_blockfrost/src/handlers/blocks.rs | 3 +- modules/rest_blockfrost/src/types.rs | 5 +++ 7 files changed, 71 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e421b439..7f2fbd81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,7 @@ dependencies = [ "caryatid_module_rest_server", "caryatid_sdk", "chrono", + "cryptoxide 0.5.1", "fraction", "futures", "gcd", @@ -1630,6 +1631,12 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" +[[package]] +name = "cryptoxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "facfae029ec4373769eb4bd936bcf537de1052abaee9f246e667c9443be6aa95" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1844,7 +1851,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb588f93c0d91b2f668849fd6d030cddb0b2e31f105963be189da5acdf492a21" dependencies = [ - "cryptoxide", + "cryptoxide 0.4.4", ] [[package]] @@ -3686,7 +3693,7 @@ dependencies = [ "base58", "bech32 0.9.1", "crc", - "cryptoxide", + "cryptoxide 0.4.4", "hex", "pallas-codec", "pallas-crypto", @@ -3745,7 +3752,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c89ea16190a87a1d8bd36923093740a2b659ed6129f4636329319a70cc4db3" dependencies = [ - "cryptoxide", + "cryptoxide 0.4.4", "hex", "pallas-codec", "rand_core 0.6.4", @@ -3860,7 +3867,7 @@ checksum = "086f428e68ab513a0445c23a345cd462dc925e37626f72f1dbb7276919f68bfa" dependencies = [ "bech32 0.9.1", "bip39", - "cryptoxide", + "cryptoxide 0.4.4", "ed25519-bip32", "pallas-crypto", "rand 0.8.5", @@ -4384,7 +4391,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/common/Cargo.toml b/common/Cargo.toml index 717ea02e..2d3a549a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -33,6 +33,7 @@ tracing = "0.1.40" futures = "0.3.31" minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } num-traits = "0.2" +cryptoxide = "0.5.1" [lib] crate-type = ["rlib"] diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 72871820..8e14b729 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,4 +1,6 @@ -use crate::{KeyHash, TxHash}; +use cryptoxide::hashing::blake2b::Blake2b; +use crate::{BlockHash, KeyHash, TxHash, serialization::Bech32WithHrp}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -62,15 +64,15 @@ pub enum BlocksStateQueryResponse { Error(String), } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize)] pub struct BlockInfo { pub timestamp: u64, pub number: u64, - pub hash: Vec, + pub hash: BlockHash, pub slot: u64, pub epoch: u64, pub epoch_slot: u64, - pub issuer_vkey: Option>, + pub issuer_vkey: Option, pub size: u64, pub tx_count: u64, pub output: Option, @@ -78,11 +80,42 @@ pub struct BlockInfo { pub block_vrf: Option>, pub op_cert: Option, pub op_cert_counter: Option, - pub previous_block: Option>, - pub next_block: Option>, + pub previous_block: Option, + pub next_block: Option, pub confirmations: u64, } +impl Serialize for BlockInfo { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer + { + let mut state = serializer.serialize_struct("BlockInfo", 17)?; + state.serialize_field("time", &self.timestamp)?; + state.serialize_field("height", &self.number)?; + state.serialize_field("slot", &self.slot)?; + state.serialize_field("epoch", &self.epoch)?; + state.serialize_field("epoch_slot", &self.epoch_slot)?; + state.serialize_field("slot_issuer", &self.issuer_vkey.clone().map(|vkey| -> String { + let mut context = Blake2b::<224>::new(); + context.update_mut(&vkey); + let digest = context.finalize().as_slice().to_owned(); + digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) + }))?; + state.serialize_field("size", &self.size)?; + state.serialize_field("tx_count", &self.tx_count)?; + state.serialize_field("output", &self.output)?; + state.serialize_field("fees", &self.fees)?; + state.serialize_field("block_vrf", &self.block_vrf.clone().map(|v| hex::encode(v)))?; + state.serialize_field("op_cert", &self.op_cert.clone().map(|v| hex::encode(v)))?; + state.serialize_field("op_cert_counter", &self.op_cert_counter)?; + state.serialize_field("previous_block", &self.previous_block)?; + state.serialize_field("next_block", &self.next_block)?; + state.serialize_field("confirmations", &self.confirmations)?; + state.end() + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct NextBlocks { pub blocks: Vec, diff --git a/common/src/types.rs b/common/src/types.rs index 1272b2b6..0033ccf7 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -82,6 +82,14 @@ pub enum BlockStatus { RolledBack, // Volatile, restarted after rollback } +/// Block hash +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct BlockHash( + #[serde_as(as = "Hex")] + pub [u8; 32], +); + /// Block info, shared across multiple messages #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BlockInfo { diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 0eefee25..7ba2b16e 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,6 +1,7 @@ mod stores; use acropolis_common::{ + BlockHash, crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ @@ -285,7 +286,7 @@ impl ChainStore { block_info.push(BlockInfo { timestamp: block.extra.timestamp, number: header.number(), - hash: header.hash().to_vec(), + hash: BlockHash(*header.hash()), slot: header.slot(), epoch: block.extra.epoch, epoch_slot: block.extra.epoch_slot, @@ -297,8 +298,8 @@ impl ChainStore { block_vrf: header.vrf_vkey().map(|key| key.to_vec()), op_cert, op_cert_counter, - previous_block: header.previous_hash().map(|h| h.to_vec()), - next_block: next_hash.map(|h| h.to_vec()), + previous_block: header.previous_hash().map(|h| BlockHash(*h)), + next_block: next_hash.map(|h| BlockHash(*h)), confirmations: latest_number - header.number(), }); diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index c44303cf..82d934d7 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -11,6 +11,7 @@ use caryatid_sdk::Context; use std::sync::Arc; use crate::handlers_config::HandlersConfig; +use crate::types::BlockInfoREST; /// Handle `/blocks/latest` pub async fn handle_blocks_latest_blockfrost( @@ -45,7 +46,7 @@ pub async fn handle_blocks_latest_blockfrost( ) .await?; - match serde_json::to_string(&block_info) { + match serde_json::to_string(&BlockInfoREST(&block_info)) { Ok(json) => Ok(RESTResponse::with_json(200, &json)), Err(e) => Ok(RESTResponse::with_text( 500, diff --git a/modules/rest_blockfrost/src/types.rs b/modules/rest_blockfrost/src/types.rs index 174886da..17178119 100644 --- a/modules/rest_blockfrost/src/types.rs +++ b/modules/rest_blockfrost/src/types.rs @@ -1,5 +1,6 @@ use acropolis_common::{ protocol_params::{Nonce, NonceVariant, ProtocolParams}, + queries::blocks::BlockInfo, queries::governance::DRepActionUpdate, rest_helper::ToCheckedF64, PoolEpochState, Relay, Vote, @@ -12,6 +13,10 @@ use std::collections::HashMap; use crate::cost_models::{PLUTUS_V1, PLUTUS_V2, PLUTUS_V3}; +// REST response structure for /blocks/latest +#[derive(Serialize)] +pub struct BlockInfoREST<'a>(pub &'a BlockInfo); + // REST response structure for /governance/dreps #[derive(Serialize)] pub struct DRepsListREST { From 89ae89bc7e8e2db0e3c663197c67dce2e05c9883 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 17 Sep 2025 17:20:05 +0100 Subject: [PATCH 12/44] Add more handlers for blocks endpoints - blocks/{hash_or_number} endpoint added - blocks/slot/{slot_number} endpoint added - blocks/epoch/{epoch_number}/slot/{slot_number} endpoint added - add missing hash field to block info output --- common/src/queries/blocks.rs | 34 ++- common/src/types.rs | 20 +- modules/chain_store/src/chain_store.rs | 5 +- .../rest_blockfrost/src/handlers/blocks.rs | 212 +++++++++++++++++- .../rest_blockfrost/src/rest_blockfrost.rs | 33 ++- 5 files changed, 276 insertions(+), 28 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 8e14b729..99913b77 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,5 +1,5 @@ +use crate::{serialization::Bech32WithHrp, BlockHash, KeyHash, TxHash}; use cryptoxide::hashing::blake2b::Blake2b; -use crate::{BlockHash, KeyHash, TxHash, serialization::Bech32WithHrp}; use serde::ser::{Serialize, SerializeStruct, Serializer}; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = @@ -43,7 +43,7 @@ pub enum BlocksStateQuery { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlockKey { - Hash(Vec), + Hash(BlockHash), Number(u64), } @@ -72,11 +72,13 @@ pub struct BlockInfo { pub slot: u64, pub epoch: u64, pub epoch_slot: u64, - pub issuer_vkey: Option, + // TODO: make a proper type for these pub keys + pub issuer_vkey: Option>, pub size: u64, pub tx_count: u64, pub output: Option, pub fees: Option, + // TODO: make a proper type for these pub keys pub block_vrf: Option>, pub op_cert: Option, pub op_cert_counter: Option, @@ -88,25 +90,35 @@ pub struct BlockInfo { impl Serialize for BlockInfo { fn serialize(&self, serializer: S) -> Result where - S: Serializer + S: Serializer, { let mut state = serializer.serialize_struct("BlockInfo", 17)?; state.serialize_field("time", &self.timestamp)?; state.serialize_field("height", &self.number)?; + state.serialize_field("hash", &self.hash)?; state.serialize_field("slot", &self.slot)?; state.serialize_field("epoch", &self.epoch)?; state.serialize_field("epoch_slot", &self.epoch_slot)?; - state.serialize_field("slot_issuer", &self.issuer_vkey.clone().map(|vkey| -> String { - let mut context = Blake2b::<224>::new(); - context.update_mut(&vkey); - let digest = context.finalize().as_slice().to_owned(); - digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) - }))?; + // TODO: handle non-SPO keys + state.serialize_field( + "slot_issuer", + &self.issuer_vkey.clone().map(|vkey| -> String { + let mut context = Blake2b::<224>::new(); + context.update_mut(&vkey); + let digest = context.finalize().as_slice().to_owned(); + digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) + }), + )?; state.serialize_field("size", &self.size)?; state.serialize_field("tx_count", &self.tx_count)?; state.serialize_field("output", &self.output)?; state.serialize_field("fees", &self.fees)?; - state.serialize_field("block_vrf", &self.block_vrf.clone().map(|v| hex::encode(v)))?; + state.serialize_field( + "block_vrf", + &self.block_vrf.clone().map(|vkey| -> String { + vkey.to_bech32_with_hrp("vrf_vk").unwrap_or(String::new()) + }), + )?; state.serialize_field("op_cert", &self.op_cert.clone().map(|v| hex::encode(v)))?; state.serialize_field("op_cert_counter", &self.op_cert_counter)?; state.serialize_field("previous_block", &self.previous_block)?; diff --git a/common/src/types.rs b/common/src/types.rs index 0033ccf7..e20a48c6 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -85,10 +85,22 @@ pub enum BlockStatus { /// Block hash #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct BlockHash( - #[serde_as(as = "Hex")] - pub [u8; 32], -); +pub struct BlockHash(#[serde_as(as = "Hex")] pub [u8; 32]); + +impl From> for BlockHash { + fn from(v: Vec) -> Self { + match v.first_chunk::<32>() { + Some(&bytes) => BlockHash(bytes), + _ => BlockHash([0u8; 32]), + } + } +} + +impl AsRef<[u8]> for BlockHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} /// Block info, shared across multiple messages #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 7ba2b16e..815ca431 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,7 +1,6 @@ mod stores; use acropolis_common::{ - BlockHash, crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ @@ -9,7 +8,7 @@ use acropolis_common::{ BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, - TxHash, + BlockHash, TxHash, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -198,7 +197,7 @@ impl ChainStore { fn get_block_by_key(store: &Arc, block_key: &BlockKey) -> Result> { match block_key { - BlockKey::Hash(hash) => store.get_block_by_hash(hash), + BlockKey::Hash(hash) => store.get_block_by_hash(hash.as_ref()), BlockKey::Number(number) => store.get_block_by_number(*number), } } diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 82d934d7..55a26490 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -2,9 +2,10 @@ use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ - blocks::{BlocksStateQuery, BlocksStateQueryResponse}, + blocks::{BlockKey, BlocksStateQuery, BlocksStateQueryResponse}, utils::query_state, }, + BlockHash, }; use anyhow::Result; use caryatid_sdk::Context; @@ -13,14 +14,43 @@ use std::sync::Arc; use crate::handlers_config::HandlersConfig; use crate::types::BlockInfoREST; +fn parse_block_key(key: &str) -> Result { + match key.len() { + 64 => match hex::decode(key) { + Ok(key) => Ok(BlockKey::Hash(BlockHash::from(key))), + Err(error) => Err(error.into()), + }, + _ => match key.parse::() { + Ok(key) => Ok(BlockKey::Number(key)), + Err(error) => Err(error.into()), + }, + } +} + +/// Handle `/blocks/latest`, `/blocks/{hash_or_number}` +pub async fn handle_blocks_latest_hash_number_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + match param.as_str() { + "latest" => handle_blocks_latest_blockfrost(context, handlers_config).await, + _ => handle_blocks_hash_or_number_blockfrost(context, param, handlers_config).await, + } +} + /// Handle `/blocks/latest` -pub async fn handle_blocks_latest_blockfrost( +async fn handle_blocks_latest_blockfrost( context: Arc>, - _: Vec, handlers_config: Arc, ) -> Result { let blocks_latest_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetLatestBlock, + BlocksStateQuery::GetLatestBlock, ))); let block_info = query_state( &context, @@ -54,3 +84,177 @@ pub async fn handle_blocks_latest_blockfrost( )), } } + +/// Handle `/blocks/{hash_or_number}` +async fn handle_blocks_hash_or_number_blockfrost( + context: Arc>, + hash_or_number: &str, + handlers_config: Arc, +) -> Result { + let block_key = match parse_block_key(hash_or_number) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + let block_info_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockInfo { block_key }, + ))); + let block_info = query_state( + &context, + &handlers_config.blocks_query_topic, + block_info_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockInfo(block_info), + )) => Ok(Some(block_info)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block by hash or number" + )) + } + }, + ) + .await?; + + match block_info { + Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block info: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + +/// Handle `/blocks/slot/{slot_number}` +pub async fn handle_blocks_slot_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let slot = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let slot = match slot.parse::() { + Ok(slot) => slot, + Err(error) => return Err(error.into()), + }; + + let block_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockBySlot { slot }, + ))); + let block_info = query_state( + &context, + &handlers_config.blocks_query_topic, + block_slot_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockBySlot(block_info), + )) => Ok(Some(block_info)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block by slot: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block by slot" + )) + } + }, + ) + .await?; + + match block_info { + Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block info: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + +/// Handle `/blocks/epoch/{epoch_number}/slot/{slot_number}` +pub async fn handle_blocks_epoch_slot_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let (epoch, slot) = match params.as_slice() { + [param1, param2] => (param1, param2), + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let epoch = match epoch.parse::() { + Ok(epoch) => epoch, + Err(error) => return Err(error.into()), + }; + + let slot = match slot.parse::() { + Ok(slot) => slot, + Err(error) => return Err(error.into()), + }; + + let block_epoch_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockByEpochSlot { epoch, slot }, + ))); + let block_info = query_state( + &context, + &handlers_config.blocks_query_topic, + block_epoch_slot_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockByEpochSlot(block_info), + )) => Ok(Some(block_info)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block by epoch slot: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block by epoch slot" + )) + } + }, + ) + .await?; + + match block_info { + Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block info: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index a4853b4b..68bc0c49 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -18,7 +18,8 @@ mod utils; use handlers::{ accounts::handle_single_account_blockfrost, blocks::{ - handle_blocks_latest_blockfrost, + handle_blocks_epoch_slot_blockfrost, handle_blocks_latest_hash_number_blockfrost, + handle_blocks_slot_blockfrost, }, epochs::{ handle_epoch_info_blockfrost, handle_epoch_next_blockfrost, handle_epoch_params_blockfrost, @@ -50,8 +51,12 @@ const DEFAULT_HANDLE_SINGLE_ACCOUNT_TOPIC: (&str, &str) = ("handle-topic-account-single", "rest.get.accounts.*"); // Blocks topics -const DEFAULT_HANDLE_BLOCKS_LATEST_TOPIC: (&str, &str) = - ("handle-blocks-latest", "rest.get.blocks.latest"); +const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC: (&str, &str) = + ("handle-blocks-latest-hash-number", "rest.get.blocks.*"); +const DEFAULT_HANDLE_BLOCKS_SLOT_TOPIC: (&str, &str) = + ("handle-blocks-slot", "rest.get.blocks.slot.*"); +const DEFAULT_HANDLE_BLOCKS_EPOCH_SLOT_TOPIC: (&str, &str) = + ("handle-blocks-epoch-slot", "rest.get.blocks.epoch.*.slot.*"); // Governance topics const DEFAULT_HANDLE_DREPS_LIST_TOPIC: (&str, &str) = @@ -172,12 +177,28 @@ impl BlockfrostREST { handle_single_account_blockfrost, ); - // Handler for /blocks/latest + // Handler for /blocks/latest, /blocks/{hash_or_number} register_handler( context.clone(), - DEFAULT_HANDLE_BLOCKS_LATEST_TOPIC, + DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC, handlers_config.clone(), - handle_blocks_latest_blockfrost, + handle_blocks_latest_hash_number_blockfrost, + ); + + // Handler for /blocks/slot/{slot_number} + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_SLOT_TOPIC, + handlers_config.clone(), + handle_blocks_slot_blockfrost, + ); + + // Handler for /blocks/epoch/{epoch_number}/slot/{slot_number} + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_EPOCH_SLOT_TOPIC, + handlers_config.clone(), + handle_blocks_epoch_slot_blockfrost, ); // Handler for /governance/dreps From 07866db85fad7407a9f9dd01ce6d70ffa45f1b56 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 19 Sep 2025 14:21:14 +0100 Subject: [PATCH 13/44] Add blocks endpoints for txs and txs/cbor --- common/src/queries/blocks.rs | 3 + common/src/types.rs | 70 ++++- modules/chain_store/src/chain_store.rs | 4 +- modules/chain_store/src/stores/fjall.rs | 2 +- modules/chain_store/src/stores/mod.rs | 2 +- .../src/genesis_bootstrapper.rs | 4 +- .../rest_blockfrost/src/handlers/blocks.rs | 239 +++++++++++++++++- .../rest_blockfrost/src/rest_blockfrost.rs | 26 ++ modules/tx_unpacker/src/map_parameters.rs | 2 +- modules/tx_unpacker/src/tx_unpacker.rs | 10 +- modules/utxo_state/src/state.rs | 1 + 11 files changed, 340 insertions(+), 23 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 99913b77..99252f9b 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,6 +1,7 @@ use crate::{serialization::Bech32WithHrp, BlockHash, KeyHash, TxHash}; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_with::{hex::Hex, serde_as}; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -148,9 +149,11 @@ pub struct BlockTransactionsCBOR { pub txs: Vec, } +#[serde_as] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BlockTransaction { pub hash: TxHash, + #[serde_as(as = "Hex")] pub cbor: Vec, } diff --git a/common/src/types.rs b/common/src/types.rs index e20a48c6..78b4b449 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -16,6 +16,7 @@ use serde_with::{hex::Hex, serde_as}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; +use std::ops::Deref; /// Protocol era #[derive( @@ -84,15 +85,24 @@ pub enum BlockStatus { /// Block hash #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, +)] pub struct BlockHash(#[serde_as(as = "Hex")] pub [u8; 32]); -impl From> for BlockHash { - fn from(v: Vec) -> Self { - match v.first_chunk::<32>() { - Some(&bytes) => BlockHash(bytes), - _ => BlockHash([0u8; 32]), - } +impl TryFrom> for BlockHash { + type Error = Vec; + + fn try_from(vec: Vec) -> Result { + Ok(BlockHash(vec.try_into()?)) + } +} + +impl TryFrom<&[u8]> for BlockHash { + type Error = std::array::TryFromSliceError; + + fn try_from(arr: &[u8]) -> Result { + Ok(BlockHash(arr.try_into()?)) } } @@ -102,6 +112,49 @@ impl AsRef<[u8]> for BlockHash { } } +impl Deref for BlockHash { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Transaction hash +#[serde_as] +#[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, +)] +pub struct TxHash(#[serde_as(as = "Hex")] pub [u8; 32]); + +impl TryFrom> for TxHash { + type Error = Vec; + + fn try_from(vec: Vec) -> Result { + Ok(TxHash(vec.try_into()?)) + } +} + +impl TryFrom<&[u8]> for TxHash { + type Error = std::array::TryFromSliceError; + + fn try_from(arr: &[u8]) -> Result { + Ok(TxHash(arr.try_into()?)) + } +} + +impl AsRef<[u8]> for TxHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for TxHash { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// Block info, shared across multiple messages #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BlockInfo { @@ -221,9 +274,6 @@ pub type GenesisKeyhash = Vec; /// Data hash used for metadata, anchors (SHA256) pub type DataHash = Vec; -/// Transaction hash -pub type TxHash = [u8; 32]; - /// Amount of Ada, in Lovelace pub type Lovelace = u64; pub type LovelaceDelta = i64; diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 815ca431..78cc738f 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -311,7 +311,7 @@ impl ChainStore { fn to_block_transactions(block: Block) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; - let hashes = decoded.txs().iter().map(|tx| TxHash::from(*tx.hash())).collect(); + let hashes = decoded.txs().iter().map(|tx| TxHash(*tx.hash())).collect(); Ok(BlockTransactions { hashes }) } @@ -321,7 +321,7 @@ impl ChainStore { .txs() .iter() .map(|tx| { - let hash = TxHash::from(*tx.hash()); + let hash = TxHash(*tx.hash()); let cbor = tx.encode(); BlockTransaction { hash, cbor } }) diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index 1161c908..149c6e75 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -217,7 +217,7 @@ impl FjallTXStore { fn insert_tx(&self, batch: &mut Batch, hash: TxHash, tx: StoredTransaction) { let bytes = minicbor::to_vec(tx).expect("infallible"); - batch.insert(&self.txs, hash, bytes); + batch.insert(&self.txs, hash.as_ref(), bytes); } } diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 4be55815..866c6199 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -34,5 +34,5 @@ pub struct ExtraBlockData { pub(crate) fn extract_tx_hashes(block: &[u8]) -> Result> { let block = pallas_traverse::MultiEraBlock::decode(block).context("could not decode block")?; - Ok(block.txs().into_iter().map(|tx| TxHash::from(*tx.hash())).collect()) + Ok(block.txs().into_iter().map(|tx| TxHash(*tx.hash())).collect()) } diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index 96443d09..311a24f9 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -7,7 +7,7 @@ use acropolis_common::{ CardanoMessage, GenesisCompleteMessage, Message, PotDeltasMessage, UTXODeltasMessage, }, Address, BlockInfo, BlockStatus, ByronAddress, Era, Lovelace, LovelaceDelta, Pot, PotDelta, - TxOutput, UTXODelta, + TxHash, TxOutput, UTXODelta, }; use anyhow::Result; use caryatid_sdk::{module, Context, Module}; @@ -121,7 +121,7 @@ impl GenesisBootstrapper { let mut total_allocated: u64 = 0; for (hash, address, amount) in gen_utxos.iter() { let tx_output = TxOutput { - tx_hash: **hash, + tx_hash: TxHash(**hash), index: 0, address: Address::Byron(ByronAddress { payload: address.payload.to_vec(), diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 55a26490..b01da3c3 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -17,7 +17,10 @@ use crate::types::BlockInfoREST; fn parse_block_key(key: &str) -> Result { match key.len() { 64 => match hex::decode(key) { - Ok(key) => Ok(BlockKey::Hash(BlockHash::from(key))), + Ok(key) => match BlockHash::try_from(key) { + Ok(block_hash) => Ok(BlockKey::Hash(block_hash)), + Err(_) => Err(anyhow::Error::msg("Invalid block hash")), + }, Err(error) => Err(error.into()), }, _ => match key.parse::() { @@ -138,6 +141,240 @@ async fn handle_blocks_hash_or_number_blockfrost( } } +/// Handle `/blocks/latest/txs`, `/blocks/{hash_or_number}/txs` +pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + match param.as_str() { + "latest" => handle_blocks_latest_transactions_blockfrost(context, handlers_config).await, + _ => { + handle_blocks_hash_or_number_transactions_blockfrost(context, param, handlers_config) + .await + } + } +} + +/// Handle `/blocks/latest/txs` +async fn handle_blocks_latest_transactions_blockfrost( + context: Arc>, + handlers_config: Arc, +) -> Result { + let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetLatestBlockTransactions, + ))); + let block_txs = query_state( + &context, + &handlers_config.blocks_query_topic, + blocks_latest_txs_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::LatestBlockTransactions(blocks_txs), + )) => Ok(blocks_txs), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving latest block transactions: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving latest block transactions" + )) + } + }, + ) + .await?; + + match serde_json::to_string(&block_txs) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block transactions: {e}"), + )), + } +} + +/// Handle `/blocks/{hash_or_number}/txs` +async fn handle_blocks_hash_or_number_transactions_blockfrost( + context: Arc>, + hash_or_number: &str, + handlers_config: Arc, +) -> Result { + let block_key = match parse_block_key(hash_or_number) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + let block_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockTransactions { block_key }, + ))); + let block_txs = query_state( + &context, + &handlers_config.blocks_query_topic, + block_txs_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockTransactions(block_txs), + )) => Ok(Some(block_txs)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block transactions by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block transactions by hash or number" + )) + } + }, + ) + .await?; + + match block_txs { + Some(block_txs) => match serde_json::to_string(&block_txs) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block transactions: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + +/// Handle `/blocks/latest/txs/cbor`, `/blocks/{hash_or_number}/txs/cbor` +pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + match param.as_str() { + "latest" => { + handle_blocks_latest_transactions_cbor_blockfrost(context, handlers_config).await + } + _ => { + handle_blocks_hash_or_number_transactions_cbor_blockfrost( + context, + param, + handlers_config, + ) + .await + } + } +} + +/// Handle `/blocks/latest/txs/cbor` +async fn handle_blocks_latest_transactions_cbor_blockfrost( + context: Arc>, + handlers_config: Arc, +) -> Result { + let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetLatestBlockTransactionsCBOR, + ))); + let block_txs_cbor = query_state( + &context, + &handlers_config.blocks_query_topic, + blocks_latest_txs_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::LatestBlockTransactionsCBOR(blocks_txs), + )) => Ok(blocks_txs), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving latest block transactions CBOR: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving latest block transactions CBOR" + )) + } + }, + ) + .await?; + + match serde_json::to_string(&block_txs_cbor) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block transactions CBOR: {e}"), + )), + } +} + +/// Handle `/blocks/{hash_or_number}/txs/cbor` +async fn handle_blocks_hash_or_number_transactions_cbor_blockfrost( + context: Arc>, + hash_or_number: &str, + handlers_config: Arc, +) -> Result { + let block_key = match parse_block_key(hash_or_number) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + let block_txs_cbor_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockTransactionsCBOR { block_key }, + ))); + let block_txs_cbor = query_state( + &context, + &handlers_config.blocks_query_topic, + block_txs_cbor_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockTransactionsCBOR(block_txs_cbor), + )) => Ok(Some(block_txs_cbor)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block transactions CBOR by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block transactions CBOR by hash or number" + )) + } + }, + ) + .await?; + + match block_txs_cbor { + Some(block_txs_cbor) => match serde_json::to_string(&block_txs_cbor) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block transactions CBOR: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + /// Handle `/blocks/slot/{slot_number}` pub async fn handle_blocks_slot_blockfrost( context: Arc>, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 68bc0c49..6513007f 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -19,6 +19,8 @@ use handlers::{ accounts::handle_single_account_blockfrost, blocks::{ handle_blocks_epoch_slot_blockfrost, handle_blocks_latest_hash_number_blockfrost, + handle_blocks_latest_hash_number_transactions_blockfrost, + handle_blocks_latest_hash_number_transactions_cbor_blockfrost, handle_blocks_slot_blockfrost, }, epochs::{ @@ -53,6 +55,14 @@ const DEFAULT_HANDLE_SINGLE_ACCOUNT_TOPIC: (&str, &str) = // Blocks topics const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC: (&str, &str) = ("handle-blocks-latest-hash-number", "rest.get.blocks.*"); +const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_TOPIC: (&str, &str) = ( + "handle-blocks-latest-hash-number-transactions", + "rest.get.blocks.*.txs", +); +const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_CBOR_TOPIC: (&str, &str) = ( + "handle-blocks-latest-hash-number-transactions-cbor", + "rest.get.blocks.*.txs.cbor", +); const DEFAULT_HANDLE_BLOCKS_SLOT_TOPIC: (&str, &str) = ("handle-blocks-slot", "rest.get.blocks.slot.*"); const DEFAULT_HANDLE_BLOCKS_EPOCH_SLOT_TOPIC: (&str, &str) = @@ -185,6 +195,22 @@ impl BlockfrostREST { handle_blocks_latest_hash_number_blockfrost, ); + // Handler for /blocks/latest/txs, /blocks/{hash_or_number}/txs + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_TOPIC, + handlers_config.clone(), + handle_blocks_latest_hash_number_transactions_blockfrost, + ); + + // Handler for /blocks/latest/txs/cbor, /blocks/{hash_or_number}/txs/cbor + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_CBOR_TOPIC, + handlers_config.clone(), + handle_blocks_latest_hash_number_transactions_cbor_blockfrost, + ); + // Handler for /blocks/slot/{slot_number} register_handler( context.clone(), diff --git a/modules/tx_unpacker/src/map_parameters.rs b/modules/tx_unpacker/src/map_parameters.rs index 5e5f1cb7..98367de0 100644 --- a/modules/tx_unpacker/src/map_parameters.rs +++ b/modules/tx_unpacker/src/map_parameters.rs @@ -145,7 +145,7 @@ pub fn map_gov_action_id(pallas_action_id: &conway::GovActionId) -> Result { let tx_output = TxOutput { - tx_hash: *tx.hash(), + tx_hash: TxHash(*tx.hash()), index: index as u64, address: address, value: output.value().coin(), @@ -214,7 +214,7 @@ impl TxUnpacker { if publish_certificates_topic.is_some() { let tx_hash = tx.hash(); for ( cert_index, cert) in certs.iter().enumerate() { - match map_parameters::map_certificate(&cert, *tx_hash, tx_index, cert_index) { + match map_parameters::map_certificate(&cert, TxHash(*tx_hash), tx_index, cert_index) { Ok(tx_cert) => { certificates.push( tx_cert); }, @@ -244,7 +244,7 @@ impl TxUnpacker { if publish_governance_procedures_topic.is_some() { if let Some(pp) = props { // Nonempty set -- governance_message.proposal_procedures will not be empty - let mut proc_id = GovActionId { transaction_id: *tx.hash(), action_index: 0 }; + let mut proc_id = GovActionId { transaction_id: TxHash(*tx.hash()), action_index: 0 }; for (action_index, pallas_governance_proposals) in pp.iter().enumerate() { match proc_id.set_action_index(action_index) .and_then (|proc_id| map_parameters::map_governance_proposals_procedures(&proc_id, &pallas_governance_proposals)) @@ -258,7 +258,7 @@ impl TxUnpacker { if let Some(pallas_vp) = votes { // Nonempty set -- governance_message.voting_procedures will not be empty match map_parameters::map_all_governance_voting_procedures(pallas_vp) { - Ok(vp) => voting_procedures.push((*tx.hash(), vp)), + Ok(vp) => voting_procedures.push((TxHash(*tx.hash()), vp)), Err(e) => error!("Cannot decode governance voting procedures in slot {}: {e}", block.slot) } } diff --git a/modules/utxo_state/src/state.rs b/modules/utxo_state/src/state.rs index 592624e8..a9ee01d7 100644 --- a/modules/utxo_state/src/state.rs +++ b/modules/utxo_state/src/state.rs @@ -25,6 +25,7 @@ impl UTXOKey { let mut hash = [0u8; 32]; // Initialize with zeros let len = hash_slice.len().min(32); // Cap at 32 bytes hash[..len].copy_from_slice(&hash_slice[..len]); // Copy input hash + let hash = TxHash(hash); Self { hash, index } } From dcae45d4431608073fc74157bda6bf1c5bb6c021 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 19 Sep 2025 15:19:22 +0100 Subject: [PATCH 14/44] Add blocks next and previous endpoints for blockfrost --- common/src/queries/blocks.rs | 4 + .../rest_blockfrost/src/handlers/blocks.rs | 126 ++++++++++++++++++ .../rest_blockfrost/src/rest_blockfrost.rs | 26 +++- 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 99252f9b..b6a0d9a5 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -9,7 +9,9 @@ pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlocksStateQuery { GetLatestBlock, + // TODO: add paging GetLatestBlockTransactions, + // TODO: add paging GetLatestBlockTransactionsCBOR, GetBlockInfo { block_key: BlockKey, @@ -34,9 +36,11 @@ pub enum BlocksStateQuery { GetBlockTransactions { block_key: BlockKey, }, + // TODO: add paging GetBlockTransactionsCBOR { block_key: BlockKey, }, + // TODO: add paging GetBlockInvolvedAddresses { block_key: BlockKey, }, diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index b01da3c3..7316932f 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -375,6 +375,132 @@ async fn handle_blocks_hash_or_number_transactions_cbor_blockfrost( } } +/// Handle `/blocks/{hash_or_number}/next` +pub async fn handle_blocks_hash_or_number_next_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let block_key = match parse_block_key(param) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + let blocks_next_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetNextBlocks { + block_key, + // TODO: Get paging values from query params + limit: 100, + skip: 0, + }, + ))); + let blocks_next = query_state( + &context, + &handlers_config.blocks_query_topic, + blocks_next_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NextBlocks(blocks_next), + )) => Ok(Some(blocks_next)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving next blocks by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving next blocks by hash or number" + )) + } + }, + ) + .await?; + + match blocks_next { + Some(blocks_next) => match serde_json::to_string(&blocks_next) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving next blocks: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + +/// Handle `/blocks/{hash_or_number}/previous` +pub async fn handle_blocks_hash_or_number_previous_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let block_key = match parse_block_key(param) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + let blocks_previous_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetPreviousBlocks { + block_key, + // TODO: Get paging values from query params + limit: 100, + skip: 0, + }, + ))); + let blocks_previous = query_state( + &context, + &handlers_config.blocks_query_topic, + blocks_previous_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::PreviousBlocks(blocks_previous), + )) => Ok(Some(blocks_previous)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving previous blocks by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving previous blocks by hash or number" + )) + } + }, + ) + .await?; + + match blocks_previous { + Some(blocks_previous) => match serde_json::to_string(&blocks_previous) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving previous blocks: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } +} + /// Handle `/blocks/slot/{slot_number}` pub async fn handle_blocks_slot_blockfrost( context: Arc>, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 6513007f..93a2c548 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -18,7 +18,9 @@ mod utils; use handlers::{ accounts::handle_single_account_blockfrost, blocks::{ - handle_blocks_epoch_slot_blockfrost, handle_blocks_latest_hash_number_blockfrost, + handle_blocks_epoch_slot_blockfrost, handle_blocks_hash_or_number_next_blockfrost, + handle_blocks_hash_or_number_previous_blockfrost, + handle_blocks_latest_hash_number_blockfrost, handle_blocks_latest_hash_number_transactions_blockfrost, handle_blocks_latest_hash_number_transactions_cbor_blockfrost, handle_blocks_slot_blockfrost, @@ -63,6 +65,12 @@ const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_CBOR_TOPIC: (&str, & "handle-blocks-latest-hash-number-transactions-cbor", "rest.get.blocks.*.txs.cbor", ); +const DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_NEXT_TOPIC: (&str, &str) = + ("handle-blocks-hash-number-next", "rest.get.blocks.*.next"); +const DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_PREVIOUS_TOPIC: (&str, &str) = ( + "handle-blocks-hash-number-previous", + "rest.get.blocks.*.previous", +); const DEFAULT_HANDLE_BLOCKS_SLOT_TOPIC: (&str, &str) = ("handle-blocks-slot", "rest.get.blocks.slot.*"); const DEFAULT_HANDLE_BLOCKS_EPOCH_SLOT_TOPIC: (&str, &str) = @@ -211,6 +219,22 @@ impl BlockfrostREST { handle_blocks_latest_hash_number_transactions_cbor_blockfrost, ); + // Handler for /blocks/{hash_or_number}/next + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_NEXT_TOPIC, + handlers_config.clone(), + handle_blocks_hash_or_number_next_blockfrost, + ); + + // Handler for /blocks/{hash_or_number}/previous + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_PREVIOUS_TOPIC, + handlers_config.clone(), + handle_blocks_hash_or_number_previous_blockfrost, + ); + // Handler for /blocks/slot/{slot_number} register_handler( context.clone(), From 3fddbd75039ab91d2be36152eba87780fc973664 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 19 Sep 2025 16:44:46 +0100 Subject: [PATCH 15/44] Standardise some blocks handler function names - also stub out addresses endpoint --- .../rest_blockfrost/src/handlers/blocks.rs | 32 +++++++++++-------- .../rest_blockfrost/src/rest_blockfrost.rs | 20 +++++++++--- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 7316932f..3743d53f 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -43,7 +43,7 @@ pub async fn handle_blocks_latest_hash_number_blockfrost( match param.as_str() { "latest" => handle_blocks_latest_blockfrost(context, handlers_config).await, - _ => handle_blocks_hash_or_number_blockfrost(context, param, handlers_config).await, + _ => handle_blocks_hash_number_blockfrost(context, param, handlers_config).await, } } @@ -89,7 +89,7 @@ async fn handle_blocks_latest_blockfrost( } /// Handle `/blocks/{hash_or_number}` -async fn handle_blocks_hash_or_number_blockfrost( +async fn handle_blocks_hash_number_blockfrost( context: Arc>, hash_or_number: &str, handlers_config: Arc, @@ -155,8 +155,7 @@ pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( match param.as_str() { "latest" => handle_blocks_latest_transactions_blockfrost(context, handlers_config).await, _ => { - handle_blocks_hash_or_number_transactions_blockfrost(context, param, handlers_config) - .await + handle_blocks_hash_number_transactions_blockfrost(context, param, handlers_config).await } } } @@ -203,7 +202,7 @@ async fn handle_blocks_latest_transactions_blockfrost( } /// Handle `/blocks/{hash_or_number}/txs` -async fn handle_blocks_hash_or_number_transactions_blockfrost( +async fn handle_blocks_hash_number_transactions_blockfrost( context: Arc>, hash_or_number: &str, handlers_config: Arc, @@ -271,12 +270,8 @@ pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( handle_blocks_latest_transactions_cbor_blockfrost(context, handlers_config).await } _ => { - handle_blocks_hash_or_number_transactions_cbor_blockfrost( - context, - param, - handlers_config, - ) - .await + handle_blocks_hash_number_transactions_cbor_blockfrost(context, param, handlers_config) + .await } } } @@ -323,7 +318,7 @@ async fn handle_blocks_latest_transactions_cbor_blockfrost( } /// Handle `/blocks/{hash_or_number}/txs/cbor` -async fn handle_blocks_hash_or_number_transactions_cbor_blockfrost( +async fn handle_blocks_hash_number_transactions_cbor_blockfrost( context: Arc>, hash_or_number: &str, handlers_config: Arc, @@ -376,7 +371,7 @@ async fn handle_blocks_hash_or_number_transactions_cbor_blockfrost( } /// Handle `/blocks/{hash_or_number}/next` -pub async fn handle_blocks_hash_or_number_next_blockfrost( +pub async fn handle_blocks_hash_number_next_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, @@ -439,7 +434,7 @@ pub async fn handle_blocks_hash_or_number_next_blockfrost( } /// Handle `/blocks/{hash_or_number}/previous` -pub async fn handle_blocks_hash_or_number_previous_blockfrost( +pub async fn handle_blocks_hash_number_previous_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, @@ -621,3 +616,12 @@ pub async fn handle_blocks_epoch_slot_blockfrost( None => Ok(RESTResponse::with_text(404, "Not found")), } } + +/// Handle `/blocks/{hash_or_number}/addresses` +pub async fn handle_blocks_hash_number_addresses_blockfrost( + _context: Arc>, + _params: Vec, + _handlers_config: Arc, +) -> Result { + Ok(RESTResponse::with_text(501, "Not implemented")) +} diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 93a2c548..15493798 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -18,8 +18,8 @@ mod utils; use handlers::{ accounts::handle_single_account_blockfrost, blocks::{ - handle_blocks_epoch_slot_blockfrost, handle_blocks_hash_or_number_next_blockfrost, - handle_blocks_hash_or_number_previous_blockfrost, + handle_blocks_epoch_slot_blockfrost, handle_blocks_hash_number_addresses_blockfrost, + handle_blocks_hash_number_next_blockfrost, handle_blocks_hash_number_previous_blockfrost, handle_blocks_latest_hash_number_blockfrost, handle_blocks_latest_hash_number_transactions_blockfrost, handle_blocks_latest_hash_number_transactions_cbor_blockfrost, @@ -75,6 +75,10 @@ const DEFAULT_HANDLE_BLOCKS_SLOT_TOPIC: (&str, &str) = ("handle-blocks-slot", "rest.get.blocks.slot.*"); const DEFAULT_HANDLE_BLOCKS_EPOCH_SLOT_TOPIC: (&str, &str) = ("handle-blocks-epoch-slot", "rest.get.blocks.epoch.*.slot.*"); +const DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_ADDRESSES_TOPIC: (&str, &str) = ( + "handle-blocks-hash-number-addresses", + "rest.get.blocks.*.addresses", +); // Governance topics const DEFAULT_HANDLE_DREPS_LIST_TOPIC: (&str, &str) = @@ -224,7 +228,7 @@ impl BlockfrostREST { context.clone(), DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_NEXT_TOPIC, handlers_config.clone(), - handle_blocks_hash_or_number_next_blockfrost, + handle_blocks_hash_number_next_blockfrost, ); // Handler for /blocks/{hash_or_number}/previous @@ -232,7 +236,7 @@ impl BlockfrostREST { context.clone(), DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_PREVIOUS_TOPIC, handlers_config.clone(), - handle_blocks_hash_or_number_previous_blockfrost, + handle_blocks_hash_number_previous_blockfrost, ); // Handler for /blocks/slot/{slot_number} @@ -251,6 +255,14 @@ impl BlockfrostREST { handle_blocks_epoch_slot_blockfrost, ); + // Handler for /blocks/{hash_or_number}/addresses + register_handler( + context.clone(), + DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_ADDRESSES_TOPIC, + handlers_config.clone(), + handle_blocks_hash_number_addresses_blockfrost, + ); + // Handler for /governance/dreps register_handler( context.clone(), From f307dfe11be43200060012b8e5b9653754cbdb1c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 26 Sep 2025 16:29:15 +0100 Subject: [PATCH 16/44] Correct placement of use for tests --- modules/chain_store/src/stores/fjall.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index 2c9e2be9..ebb09ffb 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,6 +1,6 @@ use std::{path::Path, sync::Arc}; -use acropolis_common::{BlockHash, BlockInfo, TxHash}; +use acropolis_common::{BlockInfo, TxHash}; use anyhow::Result; use config::Config; use fjall::{Batch, Keyspace, Partition}; @@ -242,6 +242,7 @@ mod tests { use crate::stores::Store; use super::*; + use acropolis_common::BlockHash; use pallas_traverse::{wellknown::GenesisValues, MultiEraBlock}; use tempfile::TempDir; From 92cc0284a5d7f82f0b0668e067e333d84d0e5dd8 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 26 Sep 2025 16:29:55 +0100 Subject: [PATCH 17/44] cargo fmt of uses --- common/src/protocol_params.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index 34630d6c..7829a377 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -1,9 +1,8 @@ use crate::{ genesis_values::GenesisValues, rational_number::{ChameleonFraction, RationalNumber}, - BlockHash, BlockVersionData, Committee, Constitution, CostModel, - DRepVotingThresholds, Era, ExUnitPrices, ExUnits, NetworkId, - PoolVotingThresholds, ProtocolConsts, + BlockHash, BlockVersionData, Committee, Constitution, CostModel, DRepVotingThresholds, Era, + ExUnitPrices, ExUnits, NetworkId, PoolVotingThresholds, ProtocolConsts, }; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; From 8c8d23dbaddfc670d303500f4ce462451bd1e3ad Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 26 Sep 2025 16:30:32 +0100 Subject: [PATCH 18/44] Add paging to blocks /next and /previous endpoints --- common/src/rest_helper.rs | 34 +++++++++++++++++ .../rest_blockfrost/src/handlers/blocks.rs | 27 +++++++++++--- .../rest_blockfrost/src/rest_blockfrost.rs | 37 +++++++++++++++++-- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/common/src/rest_helper.rs b/common/src/rest_helper.rs index c3ea34e5..135652cb 100644 --- a/common/src/rest_helper.rs +++ b/common/src/rest_helper.rs @@ -112,6 +112,40 @@ where }) } +// Handle a REST request with path and query parameters +pub fn handle_rest_with_path_and_query_parameters( + context: Arc>, + topic: &str, + handler: F, +) -> JoinHandle<()> +where + F: Fn(&[&str], HashMap) -> Fut + Send + Sync + Clone + 'static, + Fut: Future> + Send + 'static, +{ + let topic_owned = topic.to_string(); + context.handle(topic, move |message: Arc| { + let handler = handler.clone(); + let topic_owned = topic_owned.clone(); + async move { + let response = match message.as_ref() { + Message::RESTRequest(request) => { + let params_vec = + extract_params_from_topic_and_path(&topic_owned, &request.path_elements); + let params_slice: Vec<&str> = params_vec.iter().map(|s| s.as_str()).collect(); + let query_params = request.query_parameters.clone(); + match handler(¶ms_slice, query_params).await { + Ok(response) => response, + Err(error) => RESTResponse::with_text(500, &format!("{error:?}")), + } + } + _ => RESTResponse::with_text(500, "Unexpected message in REST request"), + }; + + Arc::new(Message::RESTResponse(response)) + } + }) +} + /// Extract parameters from the request path based on the topic pattern. /// Skips the first 3 parts of the topic as these are never parameters fn extract_params_from_topic_and_path(topic: &str, path_elements: &[String]) -> Vec { diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 3743d53f..2bbe75fa 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -1,5 +1,6 @@ //! REST handlers for Acropolis Blockfrost /blocks endpoints use acropolis_common::{ + extract_strict_query_params, messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ blocks::{BlockKey, BlocksStateQuery, BlocksStateQueryResponse}, @@ -9,6 +10,7 @@ use acropolis_common::{ }; use anyhow::Result; use caryatid_sdk::Context; +use std::collections::HashMap; use std::sync::Arc; use crate::handlers_config::HandlersConfig; @@ -374,6 +376,7 @@ async fn handle_blocks_hash_number_transactions_cbor_blockfrost( pub async fn handle_blocks_hash_number_next_blockfrost( context: Arc>, params: Vec, + query_params: HashMap, handlers_config: Arc, ) -> Result { let param = match params.as_slice() { @@ -386,12 +389,18 @@ pub async fn handle_blocks_hash_number_next_blockfrost( Err(error) => return Err(error), }; + extract_strict_query_params!(query_params, { + "count" => limit: Option, + "page" => page: Option, + }); + let limit = limit.unwrap_or(100); + let skip = (page.unwrap_or(1) - 1) * limit; + let blocks_next_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetNextBlocks { block_key, - // TODO: Get paging values from query params - limit: 100, - skip: 0, + limit, + skip, }, ))); let blocks_next = query_state( @@ -449,12 +458,18 @@ pub async fn handle_blocks_hash_number_previous_blockfrost( Err(error) => return Err(error), }; + extract_strict_query_params!(query_params, { + "count" => limit: Option, + "page" => page: Option, + }); + let limit = limit.unwrap_or(100); + let skip = (page.unwrap_or(1) - 1) * limit; + let blocks_previous_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetPreviousBlocks { block_key, - // TODO: Get paging values from query params - limit: 100, - skip: 0, + limit, + skip, }, ))); let blocks_previous = query_state( diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 299fa2de..2327da46 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -1,10 +1,10 @@ //! Acropolis Blockfrost-Compatible REST Module -use std::{future::Future, sync::Arc}; +use std::{collections::HashMap, future::Future, sync::Arc}; use acropolis_common::{ messages::{Message, RESTResponse}, - rest_helper::handle_rest_with_path_parameter, + rest_helper::{handle_rest_with_path_and_query_parameters, handle_rest_with_path_parameter}, }; use anyhow::Result; use caryatid_sdk::{module, Context, Module}; @@ -252,7 +252,7 @@ impl BlockfrostREST { ); // Handler for /blocks/{hash_or_number}/next - register_handler( + register_handler_with_query( context.clone(), DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_NEXT_TOPIC, handlers_config.clone(), @@ -601,3 +601,34 @@ fn register_handler( async move { handler_fn(context, params, handlers_config).await } }); } + +fn register_handler_with_query( + context: Arc>, + topic: (&str, &str), + handlers_config: Arc, + handler_fn: F, +) where + F: Fn(Arc>, Vec, HashMap, Arc) -> Fut + + Send + + Sync + + Clone + + 'static, + Fut: Future> + Send + 'static, +{ + let topic_name = context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()); + + tracing::info!("Creating request handler on '{}'", topic_name); + + handle_rest_with_path_and_query_parameters( + context.clone(), + &topic_name, + move |params, query_params| { + let context = context.clone(); + let handler_fn = handler_fn.clone(); + let params: Vec = params.iter().map(|s| s.to_string()).collect(); + let handlers_config = handlers_config.clone(); + + async move { handler_fn(context, params, query_params, handlers_config).await } + }, + ); +} From 651fc554cb754d2a1302501d344245fec59e5666 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 26 Sep 2025 17:01:17 +0100 Subject: [PATCH 19/44] Correct function parameters for previous --- modules/rest_blockfrost/src/handlers/blocks.rs | 1 + modules/rest_blockfrost/src/rest_blockfrost.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 2bbe75fa..64b4aab3 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -446,6 +446,7 @@ pub async fn handle_blocks_hash_number_next_blockfrost( pub async fn handle_blocks_hash_number_previous_blockfrost( context: Arc>, params: Vec, + query_params: HashMap, handlers_config: Arc, ) -> Result { let param = match params.as_slice() { diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 2327da46..b687108c 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -260,7 +260,7 @@ impl BlockfrostREST { ); // Handler for /blocks/{hash_or_number}/previous - register_handler( + register_handler_with_query( context.clone(), DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_PREVIOUS_TOPIC, handlers_config.clone(), From b2283459b42879d8b573092f98eb5e56d0e53c87 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 26 Sep 2025 17:53:59 +0100 Subject: [PATCH 20/44] Add missing paging parameters to queries (still need implementing) --- common/src/queries/blocks.rs | 24 +++-- common/src/queries/misc.rs | 19 ++++ common/src/queries/mod.rs | 1 + modules/chain_store/src/chain_store.rs | 30 +++++- .../rest_blockfrost/src/handlers/blocks.rs | 92 +++++++++++++++++-- .../rest_blockfrost/src/rest_blockfrost.rs | 4 +- 6 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 common/src/queries/misc.rs diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index b6a0d9a5..47d445c9 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,4 +1,4 @@ -use crate::{serialization::Bech32WithHrp, BlockHash, KeyHash, TxHash}; +use crate::{queries::misc::Order, serialization::Bech32WithHrp, BlockHash, KeyHash, TxHash}; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; @@ -9,10 +9,16 @@ pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BlocksStateQuery { GetLatestBlock, - // TODO: add paging - GetLatestBlockTransactions, - // TODO: add paging - GetLatestBlockTransactionsCBOR, + GetLatestBlockTransactions { + limit: u64, + skip: u64, + order: Order, + }, + GetLatestBlockTransactionsCBOR { + limit: u64, + skip: u64, + order: Order, + }, GetBlockInfo { block_key: BlockKey, }, @@ -35,12 +41,16 @@ pub enum BlocksStateQuery { }, GetBlockTransactions { block_key: BlockKey, + limit: u64, + skip: u64, + order: Order, }, - // TODO: add paging GetBlockTransactionsCBOR { block_key: BlockKey, + limit: u64, + skip: u64, + order: Order, }, - // TODO: add paging GetBlockInvolvedAddresses { block_key: BlockKey, }, diff --git a/common/src/queries/misc.rs b/common/src/queries/misc.rs new file mode 100644 index 00000000..48ac3681 --- /dev/null +++ b/common/src/queries/misc.rs @@ -0,0 +1,19 @@ +use std::str::FromStr; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum Order { + Asc, + Desc, +} + +impl FromStr for Order { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "asc" => Ok(Order::Asc), + "desc" => Ok(Order::Desc), + _ => Err(()), + } + } +} diff --git a/common/src/queries/mod.rs b/common/src/queries/mod.rs index da1beb83..081cac6a 100644 --- a/common/src/queries/mod.rs +++ b/common/src/queries/mod.rs @@ -11,6 +11,7 @@ pub mod governance; pub mod ledger; pub mod mempool; pub mod metadata; +pub mod misc; pub mod network; pub mod parameters; pub mod pools; diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 78cc738f..b3e7f48d 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -93,14 +93,24 @@ impl ChainStore { let info = Self::to_block_info(block, store, true)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } - BlocksStateQuery::GetLatestBlockTransactions => { + BlocksStateQuery::GetLatestBlockTransactions { + // TODO: apply these parameters + limit: _, + skip: _, + order: _, + } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; let txs = Self::to_block_transactions(block)?; Ok(BlocksStateQueryResponse::LatestBlockTransactions(txs)) } - BlocksStateQuery::GetLatestBlockTransactionsCBOR => { + BlocksStateQuery::GetLatestBlockTransactionsCBOR { + // TODO: apply these parameters + limit: _, + skip: _, + order: _, + } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; @@ -176,14 +186,26 @@ impl ChainStore { blocks: info, })) } - BlocksStateQuery::GetBlockTransactions { block_key } => { + BlocksStateQuery::GetBlockTransactions { + block_key, + // TODO: apply these parameters + limit: _, + skip: _, + order: _, + } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; let txs = Self::to_block_transactions(block)?; Ok(BlocksStateQueryResponse::BlockTransactions(txs)) } - BlocksStateQuery::GetBlockTransactionsCBOR { block_key } => { + BlocksStateQuery::GetBlockTransactionsCBOR { + block_key, + // TODO: apply these parameters + limit: _, + skip: _, + order: _, + } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 64b4aab3..9e7a55a4 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -4,6 +4,7 @@ use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ blocks::{BlockKey, BlocksStateQuery, BlocksStateQueryResponse}, + misc::Order, utils::query_state, }, BlockHash, @@ -147,6 +148,7 @@ async fn handle_blocks_hash_number_blockfrost( pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( context: Arc>, params: Vec, + query_params: HashMap, handlers_config: Arc, ) -> Result { let param = match params.as_slice() { @@ -154,10 +156,36 @@ pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), }; + extract_strict_query_params!(query_params, { + "count" => limit: Option, + "page" => page: Option, + "order" => order: Option, + }); + let limit = limit.unwrap_or(100); + let skip = (page.unwrap_or(1) - 1) * limit; + let order = order.unwrap_or(Order::Asc); + match param.as_str() { - "latest" => handle_blocks_latest_transactions_blockfrost(context, handlers_config).await, + "latest" => { + handle_blocks_latest_transactions_blockfrost( + context, + limit, + skip, + order, + handlers_config, + ) + .await + } _ => { - handle_blocks_hash_number_transactions_blockfrost(context, param, handlers_config).await + handle_blocks_hash_number_transactions_blockfrost( + context, + param, + limit, + skip, + order, + handlers_config, + ) + .await } } } @@ -165,10 +193,13 @@ pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( /// Handle `/blocks/latest/txs` async fn handle_blocks_latest_transactions_blockfrost( context: Arc>, + limit: u64, + skip: u64, + order: Order, handlers_config: Arc, ) -> Result { let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetLatestBlockTransactions, + BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order }, ))); let block_txs = query_state( &context, @@ -207,6 +238,9 @@ async fn handle_blocks_latest_transactions_blockfrost( async fn handle_blocks_hash_number_transactions_blockfrost( context: Arc>, hash_or_number: &str, + limit: u64, + skip: u64, + order: Order, handlers_config: Arc, ) -> Result { let block_key = match parse_block_key(hash_or_number) { @@ -215,7 +249,12 @@ async fn handle_blocks_hash_number_transactions_blockfrost( }; let block_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetBlockTransactions { block_key }, + BlocksStateQuery::GetBlockTransactions { + block_key, + limit, + skip, + order, + }, ))); let block_txs = query_state( &context, @@ -260,6 +299,7 @@ async fn handle_blocks_hash_number_transactions_blockfrost( pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( context: Arc>, params: Vec, + query_params: HashMap, handlers_config: Arc, ) -> Result { let param = match params.as_slice() { @@ -267,13 +307,36 @@ pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), }; + extract_strict_query_params!(query_params, { + "count" => limit: Option, + "page" => page: Option, + "order" => order: Option, + }); + let limit = limit.unwrap_or(100); + let skip = (page.unwrap_or(1) - 1) * limit; + let order = order.unwrap_or(Order::Asc); + match param.as_str() { "latest" => { - handle_blocks_latest_transactions_cbor_blockfrost(context, handlers_config).await + handle_blocks_latest_transactions_cbor_blockfrost( + context, + limit, + skip, + order, + handlers_config, + ) + .await } _ => { - handle_blocks_hash_number_transactions_cbor_blockfrost(context, param, handlers_config) - .await + handle_blocks_hash_number_transactions_cbor_blockfrost( + context, + param, + limit, + skip, + order, + handlers_config, + ) + .await } } } @@ -281,10 +344,13 @@ pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( /// Handle `/blocks/latest/txs/cbor` async fn handle_blocks_latest_transactions_cbor_blockfrost( context: Arc>, + limit: u64, + skip: u64, + order: Order, handlers_config: Arc, ) -> Result { let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetLatestBlockTransactionsCBOR, + BlocksStateQuery::GetLatestBlockTransactionsCBOR { limit, skip, order }, ))); let block_txs_cbor = query_state( &context, @@ -323,6 +389,9 @@ async fn handle_blocks_latest_transactions_cbor_blockfrost( async fn handle_blocks_hash_number_transactions_cbor_blockfrost( context: Arc>, hash_or_number: &str, + limit: u64, + skip: u64, + order: Order, handlers_config: Arc, ) -> Result { let block_key = match parse_block_key(hash_or_number) { @@ -331,7 +400,12 @@ async fn handle_blocks_hash_number_transactions_cbor_blockfrost( }; let block_txs_cbor_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetBlockTransactionsCBOR { block_key }, + BlocksStateQuery::GetBlockTransactionsCBOR { + block_key, + limit, + skip, + order, + }, ))); let block_txs_cbor = query_state( &context, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index b687108c..7e406216 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -236,7 +236,7 @@ impl BlockfrostREST { ); // Handler for /blocks/latest/txs, /blocks/{hash_or_number}/txs - register_handler( + register_handler_with_query( context.clone(), DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_TOPIC, handlers_config.clone(), @@ -244,7 +244,7 @@ impl BlockfrostREST { ); // Handler for /blocks/latest/txs/cbor, /blocks/{hash_or_number}/txs/cbor - register_handler( + register_handler_with_query( context.clone(), DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TRANSACTIONS_CBOR_TOPIC, handlers_config.clone(), From c35850f8cc456f9580b20e6b279835ce58470d91 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 1 Oct 2025 13:42:42 +0100 Subject: [PATCH 21/44] Apply paging and ordering to /txs and /txs/cbor endpoints --- common/src/queries/misc.rs | 2 +- modules/chain_store/src/chain_store.rs | 58 ++++++++++++++------------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/common/src/queries/misc.rs b/common/src/queries/misc.rs index 48ac3681..aec47185 100644 --- a/common/src/queries/misc.rs +++ b/common/src/queries/misc.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum Order { Asc, Desc, diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index b3e7f48d..fcd63f5d 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -8,6 +8,7 @@ use acropolis_common::{ BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, + queries::misc::Order, BlockHash, TxHash, }; use anyhow::{bail, Result}; @@ -94,27 +95,25 @@ impl ChainStore { Ok(BlocksStateQueryResponse::LatestBlock(info)) } BlocksStateQuery::GetLatestBlockTransactions { - // TODO: apply these parameters - limit: _, - skip: _, - order: _, + limit, + skip, + order, } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let txs = Self::to_block_transactions(block)?; + let txs = Self::to_block_transactions(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::LatestBlockTransactions(txs)) } BlocksStateQuery::GetLatestBlockTransactionsCBOR { - // TODO: apply these parameters - limit: _, - skip: _, - order: _, + limit, + skip, + order, } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let txs = Self::to_block_transactions_cbor(block)?; + let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::LatestBlockTransactionsCBOR(txs)) } BlocksStateQuery::GetBlockInfo { block_key } => { @@ -188,28 +187,26 @@ impl ChainStore { } BlocksStateQuery::GetBlockTransactions { block_key, - // TODO: apply these parameters - limit: _, - skip: _, - order: _, + limit, + skip, + order, } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let txs = Self::to_block_transactions(block)?; + let txs = Self::to_block_transactions(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::BlockTransactions(txs)) } BlocksStateQuery::GetBlockTransactionsCBOR { block_key, - // TODO: apply these parameters - limit: _, - skip: _, - order: _, + limit, + skip, + order, } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let txs = Self::to_block_transactions_cbor(block)?; + let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::BlockTransactionsCBOR(txs)) } @@ -331,17 +328,26 @@ impl ChainStore { Ok(block_info) } - fn to_block_transactions(block: Block) -> Result { + fn to_block_transactions(block: Block, limit: &u64, skip: &u64, order: &Order) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; - let hashes = decoded.txs().iter().map(|tx| TxHash(*tx.hash())).collect(); + let txs = decoded.txs(); + let txs_iter: Box> = match *order { + Order::Asc => Box::new(txs.iter()), + Order::Desc => Box::new(txs.iter().rev()), + }; + let hashes = txs_iter.skip(*skip as usize).take(*limit as usize).map(|tx| TxHash(*tx.hash())).collect(); Ok(BlockTransactions { hashes }) } - fn to_block_transactions_cbor(block: Block) -> Result { + fn to_block_transactions_cbor(block: Block, limit: &u64, skip: &u64, order: &Order) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; - let txs = decoded - .txs() - .iter() + let txs = decoded.txs(); + let txs_iter: Box> = match *order { + Order::Asc => Box::new(txs.iter()), + Order::Desc => Box::new(txs.iter().rev()), + }; + let txs = txs_iter + .skip(*skip as usize).take(*limit as usize) .map(|tx| { let hash = TxHash(*tx.hash()); let cbor = tx.encode(); From 7ccbb9f190ff5490ea69da87b837489f302164b8 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 3 Oct 2025 15:14:11 +0100 Subject: [PATCH 22/44] Add /blocks/{hash_or_number}/addresses endpoint --- Cargo.lock | 12 ++ Cargo.toml | 2 +- .../src/map_parameters.rs | 8 +- common/src/queries/blocks.rs | 29 ++++- modules/chain_store/Cargo.toml | 3 +- modules/chain_store/src/chain_store.rs | 110 ++++++++++++++---- .../rest_blockfrost/src/handlers/blocks.rs | 69 ++++++++++- .../rest_blockfrost/src/rest_blockfrost.rs | 2 +- modules/tx_unpacker/Cargo.toml | 1 + modules/tx_unpacker/src/tx_unpacker.rs | 3 +- 10 files changed, 204 insertions(+), 35 deletions(-) rename {modules/tx_unpacker => codec}/src/map_parameters.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index ab23f02d..ef3b8bfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "acropolis_codec" +version = "0.1.0" +dependencies = [ + "acropolis_common", + "anyhow", + "pallas", + "tracing", +] + [[package]] name = "acropolis_common" version = "0.3.0" @@ -87,6 +97,7 @@ dependencies = [ name = "acropolis_module_chain_store" version = "0.1.0" dependencies = [ + "acropolis_codec", "acropolis_common", "anyhow", "caryatid_sdk", @@ -321,6 +332,7 @@ dependencies = [ name = "acropolis_module_tx_unpacker" version = "0.2.1" dependencies = [ + "acropolis_codec", "acropolis_common", "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index f31942c8..1861250d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ members = [ # Process builds "processes/omnibus", # All-inclusive omnibus process "processes/replayer", # All-inclusive process to replay messages - "processes/golden_tests", #All-inclusive golden tests process + "processes/golden_tests", "codec", #All-inclusive golden tests process ] resolver = "2" diff --git a/modules/tx_unpacker/src/map_parameters.rs b/codec/src/map_parameters.rs similarity index 99% rename from modules/tx_unpacker/src/map_parameters.rs rename to codec/src/map_parameters.rs index f053baa7..47a99a61 100644 --- a/modules/tx_unpacker/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -1,12 +1,12 @@ //! Acropolis transaction unpacker module for Caryatid //! Performs conversion from Pallas library data to Acropolis -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use pallas::ledger::{ primitives::{ - alonzo, babbage, conway, ExUnitPrices as PallasExUnitPrices, Nullable, - ProtocolVersion as PallasProtocolVersion, Relay as PallasRelay, ScriptHash, - StakeCredential as PallasStakeCredential, + ExUnitPrices as PallasExUnitPrices, Nullable, ProtocolVersion as PallasProtocolVersion, + Relay as PallasRelay, ScriptHash, StakeCredential as PallasStakeCredential, alonzo, + babbage, conway, }, traverse::{MultiEraCert, MultiEraPolicyAssets, MultiEraValue}, *, diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 47d445c9..521ff2f9 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,4 +1,6 @@ -use crate::{queries::misc::Order, serialization::Bech32WithHrp, BlockHash, KeyHash, TxHash}; +use crate::{ + queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, KeyHash, TxHash, +}; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; @@ -53,6 +55,8 @@ pub enum BlocksStateQuery { }, GetBlockInvolvedAddresses { block_key: BlockKey, + limit: u64, + skip: u64, }, } @@ -172,4 +176,25 @@ pub struct BlockTransaction { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct BlockInvolvedAddresses {} +pub struct BlockInvolvedAddresses { + pub addresses: Vec, +} + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct BlockInvolvedAddress { + pub address: Address, + pub txs: Vec, +} + +impl Serialize for BlockInvolvedAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("BlockInvolvedAddress", 2)?; + state.serialize_field("address", &self.address.to_string().unwrap_or("".to_string()))?; + state.serialize_field("transactions", &self.txs)?; + state.end() + } +} + diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index 0c4b4443..34b0b3f6 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -9,6 +9,7 @@ license = "Apache-2.0" [dependencies] caryatid_sdk = "0.12" acropolis_common = { path = "../../common" } +acropolis_codec = { path = "../../codec" } anyhow = "1.0" config = "0.15.11" fjall = "2.7.0" @@ -21,4 +22,4 @@ tracing = "0.1.40" tempfile = "3" [lib] -path = "src/chain_store.rs" \ No newline at end of file +path = "src/chain_store.rs" diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index fcd63f5d..9135cebd 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,19 +1,22 @@ mod stores; +use acropolis_codec::map_parameters; use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockKey, BlockTransaction, BlockTransactions, BlockTransactionsCBOR, - BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, - DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, BlockTransaction, + BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, BlocksStateQueryResponse, + NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, - BlockHash, TxHash, + Address, BlockHash, TxHash, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; +use std::cmp::Ordering; +use std::collections::BTreeMap; use std::sync::Arc; use tracing::error; @@ -94,22 +97,14 @@ impl ChainStore { let info = Self::to_block_info(block, store, true)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } - BlocksStateQuery::GetLatestBlockTransactions { - limit, - skip, - order, - } => { + BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; let txs = Self::to_block_transactions(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::LatestBlockTransactions(txs)) } - BlocksStateQuery::GetLatestBlockTransactionsCBOR { - limit, - skip, - order, - } => { + BlocksStateQuery::GetLatestBlockTransactionsCBOR { limit, skip, order } => { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; @@ -209,8 +204,17 @@ impl ChainStore { let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::BlockTransactionsCBOR(txs)) } - - other => bail!("{other:?} not yet supported"), + BlocksStateQuery::GetBlockInvolvedAddresses { + block_key, + limit, + skip, + } => { + let Some(block) = Self::get_block_by_key(store, block_key)? else { + return Ok(BlocksStateQueryResponse::NotFound); + }; + let addresses = Self::to_block_involved_addresses(block, limit, skip)?; + Ok(BlocksStateQueryResponse::BlockInvolvedAddresses(addresses)) + } } } @@ -328,18 +332,32 @@ impl ChainStore { Ok(block_info) } - fn to_block_transactions(block: Block, limit: &u64, skip: &u64, order: &Order) -> Result { + fn to_block_transactions( + block: Block, + limit: &u64, + skip: &u64, + order: &Order, + ) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; let txs = decoded.txs(); let txs_iter: Box> = match *order { Order::Asc => Box::new(txs.iter()), Order::Desc => Box::new(txs.iter().rev()), }; - let hashes = txs_iter.skip(*skip as usize).take(*limit as usize).map(|tx| TxHash(*tx.hash())).collect(); + let hashes = txs_iter + .skip(*skip as usize) + .take(*limit as usize) + .map(|tx| TxHash(*tx.hash())) + .collect(); Ok(BlockTransactions { hashes }) } - fn to_block_transactions_cbor(block: Block, limit: &u64, skip: &u64, order: &Order) -> Result { + fn to_block_transactions_cbor( + block: Block, + limit: &u64, + skip: &u64, + order: &Order, + ) -> Result { let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; let txs = decoded.txs(); let txs_iter: Box> = match *order { @@ -347,7 +365,8 @@ impl ChainStore { Order::Desc => Box::new(txs.iter().rev()), }; let txs = txs_iter - .skip(*skip as usize).take(*limit as usize) + .skip(*skip as usize) + .take(*limit as usize) .map(|tx| { let hash = TxHash(*tx.hash()); let cbor = tx.encode(); @@ -356,4 +375,55 @@ impl ChainStore { .collect(); Ok(BlockTransactionsCBOR { txs }) } + + fn to_block_involved_addresses( + block: Block, + limit: &u64, + skip: &u64, + ) -> Result { + let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; + let mut addresses = BTreeMap::new(); + for tx in decoded.txs() { + let hash = TxHash(*tx.hash()); + for output in tx.outputs() { + match output.address() { + Ok(pallas_address) => match map_parameters::map_address(&pallas_address) { + Ok(address) => { + addresses + .entry(BechOrdAddress(address)) + .or_insert_with(Vec::new) + .push(hash.clone()); + }, + _ => (), + }, + _ => (), + } + } + } + let addresses: Vec = addresses + .into_iter() + .skip(*skip as usize) + .take(*limit as usize) + .map(|(address, txs)| BlockInvolvedAddress { + address: address.0, + txs, + }) + .collect(); + Ok(BlockInvolvedAddresses { addresses }) + } +} + +#[derive(Eq, PartialEq)] +struct BechOrdAddress(Address); + +impl Ord for BechOrdAddress { + fn cmp(&self, other: &Self) -> Ordering { + self.0.to_string().into_iter().cmp(other.0.to_string()) + } +} + +impl PartialOrd for BechOrdAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 9e7a55a4..22641d53 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -709,9 +709,70 @@ pub async fn handle_blocks_epoch_slot_blockfrost( /// Handle `/blocks/{hash_or_number}/addresses` pub async fn handle_blocks_hash_number_addresses_blockfrost( - _context: Arc>, - _params: Vec, - _handlers_config: Arc, + context: Arc>, + params: Vec, + query_params: HashMap, + handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let block_key = match parse_block_key(param) { + Ok(block_key) => block_key, + Err(error) => return Err(error), + }; + + extract_strict_query_params!(query_params, { + "count" => limit: Option, + "page" => page: Option, + }); + let limit = limit.unwrap_or(100); + let skip = (page.unwrap_or(1) - 1) * limit; + + let block_involved_addresses_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetBlockInvolvedAddresses { + block_key, + limit, + skip, + }, + ))); + let block_addresses = query_state( + &context, + &handlers_config.blocks_query_topic, + block_involved_addresses_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::BlockInvolvedAddresses(block_addresses), + )) => Ok(Some(block_addresses)), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::NotFound, + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving block addresses by hash or number: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving block addresses by hash or number" + )) + } + }, + ) + .await?; + + match block_addresses { + Some(block_addresses) => match serde_json::to_string(&block_addresses) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while retrieving block addresses: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + } } diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 7e406216..f90d955a 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -284,7 +284,7 @@ impl BlockfrostREST { ); // Handler for /blocks/{hash_or_number}/addresses - register_handler( + register_handler_with_query( context.clone(), DEFAULT_HANDLE_BLOCKS_HASH_NUMBER_ADDRESSES_TOPIC, handlers_config.clone(), diff --git a/modules/tx_unpacker/Cargo.toml b/modules/tx_unpacker/Cargo.toml index f4faf903..8619cb78 100644 --- a/modules/tx_unpacker/Cargo.toml +++ b/modules/tx_unpacker/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" [dependencies] acropolis_common = { path = "../../common" } +acropolis_codec = { path = "../../codec" } caryatid_sdk = { workspace = true } diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 0e5b4b84..0bb14301 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -1,6 +1,7 @@ //! Acropolis transaction unpacker module for Caryatid //! Unpacks transaction bodies into UTXO events +use acropolis_codec::*; use acropolis_common::{ messages::{ AssetDeltasMessage, BlockFeesMessage, CardanoMessage, GovernanceProceduresMessage, Message, @@ -19,8 +20,6 @@ use pallas::ledger::primitives::KeyValuePairs; use pallas::ledger::{primitives, traverse, traverse::MultiEraTx}; use tracing::{debug, error, info, info_span, Instrument}; -mod map_parameters; - const DEFAULT_SUBSCRIBE_TOPIC: &str = "cardano.txs"; const CIP25_METADATA_LABEL: u64 = 721; From 60ada5dca797b5988818b0947b5f38184928f1bf Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 8 Oct 2025 19:51:25 +0100 Subject: [PATCH 23/44] Add delegates to protocol params - used to match for non-SPO issuers - builds, but probably doesn't work yet --- Cargo.lock | 3 + common/src/protocol_params.rs | 8 +- common/src/queries/blocks.rs | 31 +++--- common/src/types.rs | 13 +++ modules/accounts_state/src/monetary.rs | 7 +- modules/chain_store/Cargo.toml | 2 + modules/chain_store/src/chain_store.rs | 95 ++++++++++++++++--- modules/parameters_state/Cargo.toml | 1 + .../parameters_state/src/genesis_params.rs | 13 ++- 9 files changed, 146 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef3b8bfb..6ba84363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,11 @@ dependencies = [ "config", "fjall", "hex", + "imbl", "minicbor 0.26.5", "pallas-traverse", "tempfile", + "tokio", "tracing", ] @@ -219,6 +221,7 @@ dependencies = [ "acropolis_common", "anyhow", "async-trait", + "base64 0.22.1", "blake2 0.10.6", "caryatid_sdk", "config", diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index 7829a377..de645b26 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -2,12 +2,13 @@ use crate::{ genesis_values::GenesisValues, rational_number::{ChameleonFraction, RationalNumber}, BlockHash, BlockVersionData, Committee, Constitution, CostModel, DRepVotingThresholds, Era, - ExUnitPrices, ExUnits, NetworkId, PoolVotingThresholds, ProtocolConsts, -}; + ExUnitPrices, ExUnits, GenesisDelegate, HeavyDelegate, NetworkId, + PoolVotingThresholds, ProtocolConsts, }; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use chrono::{DateTime, Utc}; use serde_with::serde_as; +use std::collections::HashMap; use std::ops::Deref; #[derive(Debug, Default, PartialEq, Clone, serde::Serialize, serde::Deserialize)] @@ -29,6 +30,7 @@ pub struct ByronParams { pub fts_seed: Option>, pub protocol_consts: ProtocolConsts, pub start_time: u64, + pub heavy_delegation: HashMap, HeavyDelegate>, } // @@ -123,6 +125,8 @@ pub struct ShelleyParams { pub slots_per_kes_period: u32, pub system_start: DateTime, pub update_quorum: u32, + + pub gen_delegs: HashMap, GenesisDelegate>, } #[serde_as] diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 521ff2f9..8b16644f 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,5 +1,5 @@ use crate::{ - queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, KeyHash, TxHash, + queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, GenesisDelegate, HeavyDelegate, KeyHash, TxHash, }; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; @@ -83,6 +83,13 @@ pub enum BlocksStateQueryResponse { Error(String), } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum BlockIssuer { + HeavyDelegate(HeavyDelegate), + GenesisDelegate(GenesisDelegate), + SPO(Vec), +} + #[derive(Debug, Clone, serde::Deserialize)] pub struct BlockInfo { pub timestamp: u64, @@ -91,8 +98,7 @@ pub struct BlockInfo { pub slot: u64, pub epoch: u64, pub epoch_slot: u64, - // TODO: make a proper type for these pub keys - pub issuer_vkey: Option>, + pub issuer: Option, pub size: u64, pub tx_count: u64, pub output: Option, @@ -118,14 +124,17 @@ impl Serialize for BlockInfo { state.serialize_field("slot", &self.slot)?; state.serialize_field("epoch", &self.epoch)?; state.serialize_field("epoch_slot", &self.epoch_slot)?; - // TODO: handle non-SPO keys - state.serialize_field( - "slot_issuer", - &self.issuer_vkey.clone().map(|vkey| -> String { - let mut context = Blake2b::<224>::new(); - context.update_mut(&vkey); - let digest = context.finalize().as_slice().to_owned(); - digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) + state.serialize_field("slot_issuer", &self.issuer.clone().map(|vkey| -> String { + match vkey { + BlockIssuer::HeavyDelegate(_) => "Byron genesis slot issuer".to_string(), + BlockIssuer::GenesisDelegate(_) => "Shelley genesis slot issuer".to_string(), + BlockIssuer::SPO(vkey) => { + let mut context = Blake2b::<224>::new(); + context.update_mut(&vkey); + let digest = context.finalize().as_slice().to_owned(); + digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) + }, + } }), )?; state.serialize_field("size", &self.size)?; diff --git a/common/src/types.rs b/common/src/types.rs index 06bd927c..edf7022e 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -1226,6 +1226,19 @@ pub struct BlockVersionData { pub update_vote_thd: u64, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct HeavyDelegate { + pub cert: Vec, + pub delegate_pk: Vec, + pub issuer_pk: Vec, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GenesisDelegate { + pub delegate: Vec, + pub vrf: Vec, +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ProtocolConsts { pub k: usize, diff --git a/modules/accounts_state/src/monetary.rs b/modules/accounts_state/src/monetary.rs index abff1be6..1a1a6b92 100644 --- a/modules/accounts_state/src/monetary.rs +++ b/modules/accounts_state/src/monetary.rs @@ -106,12 +106,14 @@ fn calculate_monetary_expansion( #[cfg(test)] mod tests { use super::*; - use acropolis_common::protocol_params::{ - Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams, + use acropolis_common::{ + protocol_params::{ Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams}, + types::GenesisDelegate, }; use acropolis_common::rational_number::rational_number_from_f32; use acropolis_common::NetworkId; use chrono::{DateTime, Utc}; + use std::collections::HashMap; // Known values at start of Shelley - from Java reference and DBSync const EPOCH_208_RESERVES: Lovelace = 13_888_022_852_926_644; @@ -164,6 +166,7 @@ mod tests { slots_per_kes_period: 129600, system_start: DateTime::::default(), update_quorum: 5, + gen_delegs: HashMap::, GenesisDelegate>::new(), } } diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index 34b0b3f6..c11c0cd1 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -17,6 +17,8 @@ hex = "0.4" minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } pallas-traverse = "0.32.1" tracing = "0.1.40" +tokio.workspace = true +imbl.workspace = true [dev-dependencies] tempfile = "3" diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 9135cebd..b2156723 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -5,24 +5,28 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, BlockTransaction, + BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockIssuer, BlockKey, BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, + state_history::{StateHistory, StateHistoryStore}, Address, BlockHash, TxHash, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; +use imbl::HashMap; use std::cmp::Ordering; use std::collections::BTreeMap; use std::sync::Arc; +use tokio::sync::Mutex; use tracing::error; use crate::stores::{fjall::FjallStore, Block, Store}; const DEFAULT_BLOCKS_TOPIC: &str = "cardano.block.body"; +const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: &str = "cardano.protocol.parameters"; const DEFAULT_STORE: &str = "fjall"; #[module( @@ -36,6 +40,8 @@ impl ChainStore { pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { let new_blocks_topic = config.get_string("blocks-topic").unwrap_or(DEFAULT_BLOCKS_TOPIC.to_string()); + let params_topic = + config.get_string("protocol-parameters-topic").unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_TOPIC.to_string()); let block_queries_topic = config .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); @@ -46,23 +52,31 @@ impl ChainStore { _ => bail!("Unknown store type {store_type}"), }; + let history = Arc::new(Mutex::new(StateHistory::::new("chain_store", StateHistoryStore::default_epoch_store()))); + history.lock().await.commit_forced(State::new()); + let query_store = store.clone(); + let query_history = history.clone(); context.handle(&block_queries_topic, move |req| { let query_store = query_store.clone(); + let query_history = query_history.clone(); async move { let Message::StateQuery(StateQuery::Blocks(query)) = req.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error("Invalid message for blocks-state".into()), ))); }; - let res = Self::handle_blocks_query(&query_store, query) + let Some(delegates) = query_history.lock().await.current().map(|s| s.delegates.clone()) else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(BlocksStateQueryResponse::Error("unitialised state".to_string())))); }; + let res = Self::handle_blocks_query(&query_store, &delegates, &query) .unwrap_or_else(|err| BlocksStateQueryResponse::Error(err.to_string())); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) } }); let mut new_blocks_subscription = context.subscribe(&new_blocks_topic).await?; + let mut params_subscription = context.subscribe(¶ms_topic).await?; context.run(async move { + let mut params_message = params_subscription.read(); loop { let Ok((_, message)) = new_blocks_subscription.read().await else { return; @@ -71,6 +85,24 @@ impl ChainStore { if let Err(err) = Self::handle_new_block(&store, &message) { error!("Could not insert block: {err}"); } + + match message.as_ref() { + Message::Cardano((block_info, _)) => { + if block_info.new_epoch { + let Ok((_, message)) = params_message.await else { + return; + }; + let mut history = history.lock().await; + let mut state = history.get_current_state(); + if !Self::handle_new_params(&mut state.delegates, message).is_ok() { + return; + }; + history.commit(block_info.number, state); + params_message = params_subscription.read(); + } + } + _ => (), + } } }); @@ -87,6 +119,7 @@ impl ChainStore { fn handle_blocks_query( store: &Arc, + delegates: &HashMap, BlockIssuer>, query: &BlocksStateQuery, ) -> Result { match query { @@ -94,7 +127,7 @@ impl ChainStore { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, true)?; + let info = Self::to_block_info(block, store, &delegates, true)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order } => { @@ -115,21 +148,21 @@ impl ChainStore { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, false)?; + let info = Self::to_block_info(block, store, &delegates, false)?; Ok(BlocksStateQueryResponse::BlockInfo(info)) } BlocksStateQuery::GetBlockBySlot { slot } => { let Some(block) = store.get_block_by_slot(*slot)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, false)?; + let info = Self::to_block_info(block, store, &delegates, false)?; Ok(BlocksStateQueryResponse::BlockBySlot(info)) } BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { let Some(block) = store.get_block_by_epoch_slot(*epoch, *slot)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, false)?; + let info = Self::to_block_info(block, store, &delegates, false)?; Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) } BlocksStateQuery::GetNextBlocks { @@ -149,7 +182,7 @@ impl ChainStore { let min_number = number + 1 + skip; let max_number = min_number + limit - 1; let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, false)?; + let info = Self::to_block_info_bulk(blocks, store, delegates, false)?; Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { blocks: info, })) @@ -175,7 +208,7 @@ impl ChainStore { }; let min_number = max_number.saturating_sub(limit - 1); let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, false)?; + let info = Self::to_block_info_bulk(blocks, store, delegates, false)?; Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { blocks: info, })) @@ -229,15 +262,16 @@ impl ChainStore { Ok(pallas_traverse::MultiEraBlock::decode(&block.bytes)?.number()) } - fn to_block_info(block: Block, store: &Arc, is_latest: bool) -> Result { + fn to_block_info(block: Block, store: &Arc, delegates: &HashMap, BlockIssuer>, is_latest: bool) -> Result { let blocks = vec![block]; - let mut info = Self::to_block_info_bulk(blocks, store, is_latest)?; + let mut info = Self::to_block_info_bulk(blocks, store, &delegates, is_latest)?; Ok(info.remove(0)) } fn to_block_info_bulk( blocks: Vec, store: &Arc, + delegates: &HashMap, BlockIssuer>, final_block_is_latest: bool, ) -> Result> { if blocks.is_empty() { @@ -312,7 +346,7 @@ impl ChainStore { slot: header.slot(), epoch: block.extra.epoch, epoch_slot: block.extra.epoch_slot, - issuer_vkey: header.issuer_vkey().map(|key| key.to_vec()), + issuer: header.issuer_vkey().map(|key| Self::vkey_to_block_issuer(key.to_vec(), delegates)), size: block.bytes.len() as u64, tx_count: decoded.tx_count() as u64, output, @@ -411,6 +445,45 @@ impl ChainStore { .collect(); Ok(BlockInvolvedAddresses { addresses }) } + + fn handle_new_params(delegates: &mut HashMap, BlockIssuer>, message: Arc) -> Result<()> { + match message.as_ref() { + Message::Cardano((block_info, CardanoMessage::ProtocolParams(params))) => { + if let Some(byron) = ¶ms.params.byron { + for (k, deleg) in &byron.heavy_delegation { + delegates.insert(k.clone(), BlockIssuer::HeavyDelegate(deleg.clone())); + } + } + if let Some(shelley) = ¶ms.params.shelley { + for (k, deleg) in &shelley.gen_delegs { + delegates.insert(k.clone(), BlockIssuer::GenesisDelegate(deleg.clone())); + } + } + }, + _ => (), + } + Ok(()) + } + + fn vkey_to_block_issuer(vkey: Vec, delegates: &HashMap, BlockIssuer>) -> BlockIssuer { + match delegates.get(&vkey) { + Some(delegate) => delegate.clone(), + None => BlockIssuer::SPO(vkey), + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct State { + pub delegates: HashMap, BlockIssuer>, +} + +impl State { + pub fn new() -> Self { + Self { + delegates: HashMap::new(), + } + } } #[derive(Eq, PartialEq)] diff --git a/modules/parameters_state/Cargo.toml b/modules/parameters_state/Cargo.toml index 3b36030e..f020a06b 100644 --- a/modules/parameters_state/Cargo.toml +++ b/modules/parameters_state/Cargo.toml @@ -26,6 +26,7 @@ serde_json = { workspace = true } serde_with = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +base64 = "0.22.1" [build-dependencies] reqwest = { version = "0.11", features = ["blocking"] } diff --git a/modules/parameters_state/src/genesis_params.rs b/modules/parameters_state/src/genesis_params.rs index 67251a96..329733d8 100644 --- a/modules/parameters_state/src/genesis_params.rs +++ b/modules/parameters_state/src/genesis_params.rs @@ -3,9 +3,10 @@ use acropolis_common::{ protocol_params::{AlonzoParams, BabbageParams, ByronParams, ConwayParams, ShelleyParams}, rational_number::{rational_number_from_f32, RationalNumber}, Anchor, BlockVersionData, Committee, Constitution, CostModel, Credential, DRepVotingThresholds, - Era, PoolVotingThresholds, ProtocolConsts, SoftForkRule, TxFeePolicy, + Era, HeavyDelegate, PoolVotingThresholds, ProtocolConsts, SoftForkRule, TxFeePolicy, }; use anyhow::{anyhow, bail, Result}; +use base64::prelude::*; use hex::decode; use pallas::ledger::configs::*; use serde::Deserialize; @@ -178,11 +179,21 @@ fn map_protocol_consts(c: &byron::ProtocolConsts) -> Result { } fn map_byron(genesis: &byron::GenesisFile) -> Result { + let heavy_delegation = genesis.heavy_delegation.iter().map(|(k, v)| { + let k = hex::decode(k)?; + let v = HeavyDelegate { + cert: hex::decode(v.cert.clone())?, + delegate_pk: BASE64_STANDARD.decode(v.delegate_pk.clone())?, + issuer_pk: BASE64_STANDARD.decode(v.issuer_pk.clone())?, + }; + Ok::<(Vec, HeavyDelegate), anyhow::Error>((k, v)) + }).collect::>()?; Ok(ByronParams { block_version_data: map_block_version_data(&genesis.block_version_data)?, fts_seed: genesis.fts_seed.as_ref().map(|s| decode_hex_string(s, 42)).transpose()?, protocol_consts: map_protocol_consts(&genesis.protocol_consts)?, start_time: genesis.start_time, + heavy_delegation, }) } From eb5942c7ad77619c61108275cd2eae4f2130abd3 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 10 Oct 2025 10:34:05 +0100 Subject: [PATCH 24/44] Add serde serialisation hints to fix Shelley params read/write --- common/src/protocol_params.rs | 3 ++- common/src/types.rs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index de645b26..e866fdb8 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -7,7 +7,7 @@ use crate::{ use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use chrono::{DateTime, Utc}; -use serde_with::serde_as; +use serde_with::{ hex::Hex, serde_as }; use std::collections::HashMap; use std::ops::Deref; @@ -126,6 +126,7 @@ pub struct ShelleyParams { pub system_start: DateTime, pub update_quorum: u32, + #[serde_as(as = "HashMap")] pub gen_delegs: HashMap, GenesisDelegate>, } diff --git a/common/src/types.rs b/common/src/types.rs index 6e92e8a5..bcfbe0b1 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -1293,9 +1293,12 @@ pub struct HeavyDelegate { pub issuer_pk: Vec, } +#[serde_as] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct GenesisDelegate { + #[serde_as(as = "Hex")] pub delegate: Vec, + #[serde_as(as = "Hex")] pub vrf: Vec, } From 5120ce7a8e6eab0b97f20b0a62ed866f7dcd6044 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 10 Oct 2025 10:35:17 +0100 Subject: [PATCH 25/44] cargo fmt cleanup --- common/src/protocol_params.rs | 7 +-- common/src/queries/blocks.rs | 15 ++++-- modules/accounts_state/src/monetary.rs | 8 +-- modules/chain_store/src/chain_store.rs | 49 ++++++++++++++----- .../parameters_state/src/genesis_params.rs | 22 +++++---- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index e866fdb8..3c2a0283 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -2,12 +2,13 @@ use crate::{ genesis_values::GenesisValues, rational_number::{ChameleonFraction, RationalNumber}, BlockHash, BlockVersionData, Committee, Constitution, CostModel, DRepVotingThresholds, Era, - ExUnitPrices, ExUnits, GenesisDelegate, HeavyDelegate, NetworkId, - PoolVotingThresholds, ProtocolConsts, }; + ExUnitPrices, ExUnits, GenesisDelegate, HeavyDelegate, NetworkId, PoolVotingThresholds, + ProtocolConsts, +}; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use chrono::{DateTime, Utc}; -use serde_with::{ hex::Hex, serde_as }; +use serde_with::{hex::Hex, serde_as}; use std::collections::HashMap; use std::ops::Deref; diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 8b16644f..802e1c64 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,5 +1,6 @@ use crate::{ - queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, GenesisDelegate, HeavyDelegate, KeyHash, TxHash, + queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, GenesisDelegate, + HeavyDelegate, KeyHash, TxHash, }; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; @@ -124,7 +125,9 @@ impl Serialize for BlockInfo { state.serialize_field("slot", &self.slot)?; state.serialize_field("epoch", &self.epoch)?; state.serialize_field("epoch_slot", &self.epoch_slot)?; - state.serialize_field("slot_issuer", &self.issuer.clone().map(|vkey| -> String { + state.serialize_field( + "slot_issuer", + &self.issuer.clone().map(|vkey| -> String { match vkey { BlockIssuer::HeavyDelegate(_) => "Byron genesis slot issuer".to_string(), BlockIssuer::GenesisDelegate(_) => "Shelley genesis slot issuer".to_string(), @@ -133,7 +136,7 @@ impl Serialize for BlockInfo { context.update_mut(&vkey); let digest = context.finalize().as_slice().to_owned(); digest.to_bech32_with_hrp("pool").unwrap_or(String::new()) - }, + } } }), )?; @@ -201,9 +204,11 @@ impl Serialize for BlockInvolvedAddress { S: Serializer, { let mut state = serializer.serialize_struct("BlockInvolvedAddress", 2)?; - state.serialize_field("address", &self.address.to_string().unwrap_or("".to_string()))?; + state.serialize_field( + "address", + &self.address.to_string().unwrap_or("".to_string()), + )?; state.serialize_field("transactions", &self.txs)?; state.end() } } - diff --git a/modules/accounts_state/src/monetary.rs b/modules/accounts_state/src/monetary.rs index 1a1a6b92..2ddf9955 100644 --- a/modules/accounts_state/src/monetary.rs +++ b/modules/accounts_state/src/monetary.rs @@ -106,12 +106,12 @@ fn calculate_monetary_expansion( #[cfg(test)] mod tests { use super::*; + use acropolis_common::rational_number::rational_number_from_f32; + use acropolis_common::NetworkId; use acropolis_common::{ - protocol_params::{ Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams}, + protocol_params::{Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams}, types::GenesisDelegate, }; - use acropolis_common::rational_number::rational_number_from_f32; - use acropolis_common::NetworkId; use chrono::{DateTime, Utc}; use std::collections::HashMap; @@ -166,7 +166,7 @@ mod tests { slots_per_kes_period: 129600, system_start: DateTime::::default(), update_quorum: 5, - gen_delegs: HashMap::, GenesisDelegate>::new(), + gen_delegs: HashMap::, GenesisDelegate>::new(), } } diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index b2156723..7dc500de 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -5,9 +5,9 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockIssuer, BlockKey, BlockTransaction, - BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, BlocksStateQueryResponse, - NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockIssuer, BlockKey, + BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, + BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, state_history::{StateHistory, StateHistoryStore}, @@ -40,8 +40,9 @@ impl ChainStore { pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { let new_blocks_topic = config.get_string("blocks-topic").unwrap_or(DEFAULT_BLOCKS_TOPIC.to_string()); - let params_topic = - config.get_string("protocol-parameters-topic").unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_TOPIC.to_string()); + let params_topic = config + .get_string("protocol-parameters-topic") + .unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_TOPIC.to_string()); let block_queries_topic = config .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); @@ -52,7 +53,10 @@ impl ChainStore { _ => bail!("Unknown store type {store_type}"), }; - let history = Arc::new(Mutex::new(StateHistory::::new("chain_store", StateHistoryStore::default_epoch_store()))); + let history = Arc::new(Mutex::new(StateHistory::::new( + "chain_store", + StateHistoryStore::default_epoch_store(), + ))); history.lock().await.commit_forced(State::new()); let query_store = store.clone(); @@ -66,7 +70,13 @@ impl ChainStore { BlocksStateQueryResponse::Error("Invalid message for blocks-state".into()), ))); }; - let Some(delegates) = query_history.lock().await.current().map(|s| s.delegates.clone()) else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(BlocksStateQueryResponse::Error("unitialised state".to_string())))); }; + let Some(delegates) = + query_history.lock().await.current().map(|s| s.delegates.clone()) + else { + return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error("unitialised state".to_string()), + ))); + }; let res = Self::handle_blocks_query(&query_store, &delegates, &query) .unwrap_or_else(|err| BlocksStateQueryResponse::Error(err.to_string())); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) @@ -262,7 +272,12 @@ impl ChainStore { Ok(pallas_traverse::MultiEraBlock::decode(&block.bytes)?.number()) } - fn to_block_info(block: Block, store: &Arc, delegates: &HashMap, BlockIssuer>, is_latest: bool) -> Result { + fn to_block_info( + block: Block, + store: &Arc, + delegates: &HashMap, BlockIssuer>, + is_latest: bool, + ) -> Result { let blocks = vec![block]; let mut info = Self::to_block_info_bulk(blocks, store, &delegates, is_latest)?; Ok(info.remove(0)) @@ -346,7 +361,9 @@ impl ChainStore { slot: header.slot(), epoch: block.extra.epoch, epoch_slot: block.extra.epoch_slot, - issuer: header.issuer_vkey().map(|key| Self::vkey_to_block_issuer(key.to_vec(), delegates)), + issuer: header + .issuer_vkey() + .map(|key| Self::vkey_to_block_issuer(key.to_vec(), delegates)), size: block.bytes.len() as u64, tx_count: decoded.tx_count() as u64, output, @@ -427,7 +444,7 @@ impl ChainStore { .entry(BechOrdAddress(address)) .or_insert_with(Vec::new) .push(hash.clone()); - }, + } _ => (), }, _ => (), @@ -446,7 +463,10 @@ impl ChainStore { Ok(BlockInvolvedAddresses { addresses }) } - fn handle_new_params(delegates: &mut HashMap, BlockIssuer>, message: Arc) -> Result<()> { + fn handle_new_params( + delegates: &mut HashMap, BlockIssuer>, + message: Arc, + ) -> Result<()> { match message.as_ref() { Message::Cardano((block_info, CardanoMessage::ProtocolParams(params))) => { if let Some(byron) = ¶ms.params.byron { @@ -459,13 +479,16 @@ impl ChainStore { delegates.insert(k.clone(), BlockIssuer::GenesisDelegate(deleg.clone())); } } - }, + } _ => (), } Ok(()) } - fn vkey_to_block_issuer(vkey: Vec, delegates: &HashMap, BlockIssuer>) -> BlockIssuer { + fn vkey_to_block_issuer( + vkey: Vec, + delegates: &HashMap, BlockIssuer>, + ) -> BlockIssuer { match delegates.get(&vkey) { Some(delegate) => delegate.clone(), None => BlockIssuer::SPO(vkey), diff --git a/modules/parameters_state/src/genesis_params.rs b/modules/parameters_state/src/genesis_params.rs index 329733d8..d8327c8f 100644 --- a/modules/parameters_state/src/genesis_params.rs +++ b/modules/parameters_state/src/genesis_params.rs @@ -179,15 +179,19 @@ fn map_protocol_consts(c: &byron::ProtocolConsts) -> Result { } fn map_byron(genesis: &byron::GenesisFile) -> Result { - let heavy_delegation = genesis.heavy_delegation.iter().map(|(k, v)| { - let k = hex::decode(k)?; - let v = HeavyDelegate { - cert: hex::decode(v.cert.clone())?, - delegate_pk: BASE64_STANDARD.decode(v.delegate_pk.clone())?, - issuer_pk: BASE64_STANDARD.decode(v.issuer_pk.clone())?, - }; - Ok::<(Vec, HeavyDelegate), anyhow::Error>((k, v)) - }).collect::>()?; + let heavy_delegation = genesis + .heavy_delegation + .iter() + .map(|(k, v)| { + let k = hex::decode(k)?; + let v = HeavyDelegate { + cert: hex::decode(v.cert.clone())?, + delegate_pk: BASE64_STANDARD.decode(v.delegate_pk.clone())?, + issuer_pk: BASE64_STANDARD.decode(v.issuer_pk.clone())?, + }; + Ok::<(Vec, HeavyDelegate), anyhow::Error>((k, v)) + }) + .collect::>()?; Ok(ByronParams { block_version_data: map_block_version_data(&genesis.block_version_data)?, fts_seed: genesis.fts_seed.as_ref().map(|s| decode_hex_string(s, 42)).transpose()?, From e50bd8ca148ccd06b36e77fe8ab7209e6b582104 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 10 Oct 2025 16:35:22 +0100 Subject: [PATCH 26/44] Correct matching of byron and shelley genesis slot issuers --- Cargo.lock | 306 ++++++++++++++++++++----- Cargo.toml | 8 +- codec/Cargo.toml | 14 ++ codec/src/block.rs | 44 ++++ codec/src/lib.rs | 2 + modules/chain_store/Cargo.toml | 2 +- modules/chain_store/src/chain_store.rs | 81 +++---- processes/omnibus/omnibus.toml | 2 +- 8 files changed, 351 insertions(+), 108 deletions(-) create mode 100644 codec/Cargo.toml create mode 100644 codec/src/block.rs create mode 100644 codec/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8b18a5df..f4535fa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,10 @@ version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", - "pallas", + "cryptoxide 0.5.1", + "pallas 0.33.0", + "pallas-primitives 0.33.0", + "pallas-traverse 0.33.0", "tracing", ] @@ -88,7 +91,7 @@ dependencies = [ "anyhow", "caryatid_sdk", "config", - "pallas", + "pallas 0.33.0", "tokio", "tracing", ] @@ -106,7 +109,7 @@ dependencies = [ "hex", "imbl", "minicbor 0.26.5", - "pallas-traverse", + "pallas-traverse 0.33.0", "tempfile", "tokio", "tracing", @@ -155,7 +158,7 @@ dependencies = [ "dashmap", "hex", "imbl", - "pallas", + "pallas 0.33.0", "serde", "serde_json", "tokio", @@ -172,7 +175,7 @@ dependencies = [ "caryatid_sdk", "config", "hex", - "pallas", + "pallas 0.33.0", "reqwest 0.12.23", "serde_json", "tokio", @@ -208,7 +211,7 @@ dependencies = [ "config", "mithril-client", "mithril-common", - "pallas", + "pallas 0.32.1", "reqwest 0.11.27", "serde_json", "tokio", @@ -228,7 +231,7 @@ dependencies = [ "config", "hex", "num-rational", - "pallas", + "pallas 0.33.0", "reqwest 0.11.27", "serde", "serde_json", @@ -271,7 +274,7 @@ dependencies = [ "config", "fraction", "hex", - "pallas", + "pallas 0.33.0", "serde_json", "tokio", "tracing", @@ -303,7 +306,7 @@ dependencies = [ "dashmap", "hex", "imbl", - "pallas", + "pallas 0.33.0", "rayon", "serde", "serde_json", @@ -323,7 +326,7 @@ dependencies = [ "caryatid_sdk", "config", "hex", - "pallas", + "pallas 0.33.0", "serde", "serde_json", "serde_json_any_key", @@ -344,7 +347,7 @@ dependencies = [ "config", "futures", "hex", - "pallas", + "pallas 0.33.0", "serde", "serde_json", "tokio", @@ -360,7 +363,7 @@ dependencies = [ "caryatid_sdk", "config", "crossbeam", - "pallas", + "pallas 0.33.0", "serde", "serde_json", "tokio", @@ -3813,16 +3816,33 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cc85b0d73cc19b7c1e09d6f2c5c4abfad3923671654ba6ef8fd00c9d0ee4c58" dependencies = [ - "pallas-addresses", - "pallas-codec", - "pallas-configs", - "pallas-crypto", + "pallas-addresses 0.32.1", + "pallas-codec 0.32.1", + "pallas-configs 0.32.1", + "pallas-crypto 0.32.1", "pallas-hardano", - "pallas-network", - "pallas-primitives", - "pallas-traverse", - "pallas-txbuilder", - "pallas-utxorpc", + "pallas-network 0.32.1", + "pallas-primitives 0.32.1", + "pallas-traverse 0.32.1", + "pallas-txbuilder 0.32.1", + "pallas-utxorpc 0.32.1", +] + +[[package]] +name = "pallas" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bba5e9e84978df0d42b72c3b92ead5a3b1c4852f66c08b648a5c057f58717a" +dependencies = [ + "pallas-addresses 0.33.0", + "pallas-codec 0.33.0", + "pallas-configs 0.33.0", + "pallas-crypto 0.33.0", + "pallas-network 0.33.0", + "pallas-primitives 0.33.0", + "pallas-traverse 0.33.0", + "pallas-txbuilder 0.33.0", + "pallas-utxorpc 0.33.0", ] [[package]] @@ -3836,8 +3856,24 @@ dependencies = [ "crc", "cryptoxide 0.4.4", "hex", - "pallas-codec", - "pallas-crypto", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "thiserror 1.0.69", +] + +[[package]] +name = "pallas-addresses" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f5f4dd205316335bf8eef77227e01a8a00b1fd60503d807520e93dd0362d0e" +dependencies = [ + "base58", + "bech32 0.9.1", + "crc", + "cryptoxide 0.4.4", + "hex", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", "thiserror 1.0.69", ] @@ -3849,11 +3885,27 @@ checksum = "6a861573364d48ff0952b12d3f139e05a843b8209f134a0c2b028449cb59ed68" dependencies = [ "chrono", "hex", - "pallas-addresses", - "pallas-codec", - "pallas-crypto", - "pallas-primitives", - "pallas-traverse", + "pallas-addresses 0.32.1", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "pallas-primitives 0.32.1", + "pallas-traverse 0.32.1", + "rand 0.8.5", +] + +[[package]] +name = "pallas-applying" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b196174663e1c4eb80a286b8ddca78f75fca5fc57b0baaa5b1143a6dd76ca71b" +dependencies = [ + "chrono", + "hex", + "pallas-addresses 0.33.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", + "pallas-primitives 0.33.0", + "pallas-traverse 0.33.0", "rand 0.8.5", ] @@ -3869,6 +3921,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pallas-codec" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2737b05f0dbb6d197feeb26ef15d2567e54833184bd469f5655a0537da89fa" +dependencies = [ + "hex", + "minicbor 0.25.1", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "pallas-configs" version = "0.32.1" @@ -3878,10 +3942,28 @@ dependencies = [ "base64 0.22.1", "hex", "num-rational", - "pallas-addresses", - "pallas-codec", - "pallas-crypto", - "pallas-primitives", + "pallas-addresses 0.32.1", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "pallas-primitives 0.32.1", + "serde", + "serde_json", + "serde_with 3.14.1", +] + +[[package]] +name = "pallas-configs" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4e63bff98bd71b3057a0986dc72e6ba58afaf063bce3dc8243fda5f0665726" +dependencies = [ + "base64 0.22.1", + "hex", + "num-rational", + "pallas-addresses 0.33.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", + "pallas-primitives 0.33.0", "serde", "serde_json", "serde_with 3.14.1", @@ -3895,7 +3977,22 @@ checksum = "59c89ea16190a87a1d8bd36923093740a2b659ed6129f4636329319a70cc4db3" dependencies = [ "cryptoxide 0.4.4", "hex", - "pallas-codec", + "pallas-codec 0.32.1", + "rand_core 0.6.4", + "serde", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "pallas-crypto" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0368945cd093e550febe36aef085431b1611c2e9196297cd70f4b21a4add054c" +dependencies = [ + "cryptoxide 0.4.4", + "hex", + "pallas-codec 0.33.0", "rand_core 0.6.4", "serde", "thiserror 1.0.69", @@ -3909,8 +4006,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f980c9e0579642a5c8a902231a499b826fdb8673585be9d3068eb9b04ccc980" dependencies = [ "binary-layout", - "pallas-network", - "pallas-traverse", + "pallas-network 0.32.1", + "pallas-traverse 0.32.1", "tap", "thiserror 1.0.69", "tracing", @@ -3925,8 +4022,26 @@ dependencies = [ "byteorder", "hex", "itertools 0.13.0", - "pallas-codec", - "pallas-crypto", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "rand 0.8.5", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "pallas-network" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1244da7a760a08b8a9d9a28a28112f10a7b6476d64192696a269cfd09a7ec55c" +dependencies = [ + "byteorder", + "hex", + "itertools 0.13.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", "rand 0.8.5", "socket2 0.5.10", "thiserror 1.0.69", @@ -3944,8 +4059,24 @@ dependencies = [ "bech32 0.9.1", "hex", "log", - "pallas-codec", - "pallas-crypto", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "serde", + "serde_json", +] + +[[package]] +name = "pallas-primitives" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2acde8875c43446194d387c60fe2d6a127e4f8384bef3dcabd5a04e9422429" +dependencies = [ + "base58", + "bech32 0.9.1", + "hex", + "log", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", "serde", "serde_json", ] @@ -3958,10 +4089,27 @@ checksum = "be7fbb1db75a0b6b32d1808b2cc5c7ba6dd261f289491bb86998b987b4716883" dependencies = [ "hex", "itertools 0.13.0", - "pallas-addresses", - "pallas-codec", - "pallas-crypto", - "pallas-primitives", + "pallas-addresses 0.32.1", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "pallas-primitives 0.32.1", + "paste", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "pallas-traverse" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab64895a0d94fed1ef2d99dd37e480ed0483e91eb98dcd2f94cc614fb9575173" +dependencies = [ + "hex", + "itertools 0.13.0", + "pallas-addresses 0.33.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", + "pallas-primitives 0.33.0", "paste", "serde", "thiserror 1.0.69", @@ -3974,12 +4122,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff83ae515a88b1ecf5354468d9fd3562d915e5eceb5c9467f6b1cdce60a3e9a" dependencies = [ "hex", - "pallas-addresses", - "pallas-codec", - "pallas-crypto", - "pallas-primitives", - "pallas-traverse", - "pallas-wallet", + "pallas-addresses 0.32.1", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "pallas-primitives 0.32.1", + "pallas-traverse 0.32.1", + "pallas-wallet 0.32.1", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "pallas-txbuilder" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ff1f49d99aced71b20daa68577167e1db3f0aaffe92fbc1de6df0b6002a66e" +dependencies = [ + "hex", + "pallas-addresses 0.33.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", + "pallas-primitives 0.33.0", + "pallas-traverse 0.33.0", + "pallas-wallet 0.33.0", "serde", "serde_json", "thiserror 1.0.69", @@ -3991,11 +4157,26 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "810ccda35242fef9ea583a0819da7617b6761a86c6070f16aea27ac80ad4da75" dependencies = [ - "pallas-applying", - "pallas-codec", - "pallas-crypto", - "pallas-primitives", - "pallas-traverse", + "pallas-applying 0.32.1", + "pallas-codec 0.32.1", + "pallas-crypto 0.32.1", + "pallas-primitives 0.32.1", + "pallas-traverse 0.32.1", + "prost-types", + "utxorpc-spec", +] + +[[package]] +name = "pallas-utxorpc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf89daca5ebfbcd9b5cf8b480486302ffd3401f6891d3c4f02087fd7687b94" +dependencies = [ + "pallas-applying 0.33.0", + "pallas-codec 0.33.0", + "pallas-crypto 0.33.0", + "pallas-primitives 0.33.0", + "pallas-traverse 0.33.0", "prost-types", "utxorpc-spec", ] @@ -4010,7 +4191,22 @@ dependencies = [ "bip39", "cryptoxide 0.4.4", "ed25519-bip32", - "pallas-crypto", + "pallas-crypto 0.32.1", + "rand 0.8.5", + "thiserror 1.0.69", +] + +[[package]] +name = "pallas-wallet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91b48fe1d0d07b425aed4b1c6ac5d962e0a392ccc58e2f3faa8ad250a5c364" +dependencies = [ + "bech32 0.9.1", + "bip39", + "cryptoxide 0.4.4", + "ed25519-bip32", + "pallas-crypto 0.33.0", "rand 0.8.5", "thiserror 1.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 1861250d..0e87b690 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,11 @@ config = "0.15.11" dashmap = "6.1.0" hex = "0.4" imbl = { version = "5.0.0", features = ["serde"] } -pallas = "0.32.1" -pallas-addresses = "0.32.0" -pallas-crypto = "0.32.0" +pallas = "0.33.0" +pallas-addresses = "0.33.0" +pallas-crypto = "0.33.0" +pallas-primitives = "0.33.0" +pallas-traverse = "0.33.0" serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" serde_with = { version = "3.12.0", features = ["hex"] } diff --git a/codec/Cargo.toml b/codec/Cargo.toml new file mode 100644 index 00000000..9cb1e77a --- /dev/null +++ b/codec/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "acropolis_codec" +version = "0.1.0" +edition = "2024" + +[dependencies] +acropolis_common = { path = "../common" } + +anyhow = { workspace = true } +cryptoxide = "0.5.1" +pallas = { workspace = true } +pallas-primitives = { workspace = true } +pallas-traverse = { workspace = true } +tracing = { workspace = true } diff --git a/codec/src/block.rs b/codec/src/block.rs new file mode 100644 index 00000000..9b5e3888 --- /dev/null +++ b/codec/src/block.rs @@ -0,0 +1,44 @@ +use acropolis_common::{GenesisDelegate, HeavyDelegate, queries::blocks::BlockIssuer}; +use cryptoxide::hashing::blake2b::Blake2b; +use pallas_primitives::byron::BlockSig::DlgSig; +use pallas_traverse::MultiEraHeader; +use std::collections::HashMap; + +pub fn map_to_block_issuer( + header: &MultiEraHeader, + byron_heavy_delegates: &HashMap, HeavyDelegate>, + shelley_genesis_delegates: &HashMap, GenesisDelegate>, +) -> Option { + match header.issuer_vkey() { + Some(vkey) => match header { + MultiEraHeader::ShelleyCompatible(_) => { + let mut context = Blake2b::<224>::new(); + context.update_mut(&vkey); + let digest = context.finalize().as_slice().to_owned(); + if let Some(issuer) = shelley_genesis_delegates + .values() + .find(|v| v.delegate == digest) + .map(|i| BlockIssuer::GenesisDelegate(i.clone())) + { + Some(issuer) + } else { + Some(BlockIssuer::SPO(vkey.to_vec())) + } + } + _ => Some(BlockIssuer::SPO(vkey.to_vec())), + }, + None => match header { + MultiEraHeader::Byron(_) => match header.as_byron() { + Some(block_head) => match &block_head.consensus_data.3 { + DlgSig(sig) => byron_heavy_delegates + .values() + .find(|v| v.issuer_pk == *sig.0.issuer) + .map(|i| BlockIssuer::HeavyDelegate(i.clone())), + _ => None, + }, + None => None, + }, + _ => None, + }, + } +} diff --git a/codec/src/lib.rs b/codec/src/lib.rs new file mode 100644 index 00000000..7cb7bc0d --- /dev/null +++ b/codec/src/lib.rs @@ -0,0 +1,2 @@ +pub mod block; +pub mod map_parameters; diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index c11c0cd1..3ed9fc55 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -15,7 +15,7 @@ config = "0.15.11" fjall = "2.7.0" hex = "0.4" minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } -pallas-traverse = "0.32.1" +pallas-traverse = { workspace = true } tracing = "0.1.40" tokio.workspace = true imbl.workspace = true diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 7dc500de..857a8fbb 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,24 +1,23 @@ mod stores; -use acropolis_codec::map_parameters; +use acropolis_codec::{block::map_to_block_issuer, map_parameters}; use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockIssuer, BlockKey, - BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, - BlocksStateQueryResponse, NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, + BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, BlockTransaction, + BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, BlocksStateQueryResponse, + NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, state_history::{StateHistory, StateHistoryStore}, - Address, BlockHash, TxHash, + Address, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; -use imbl::HashMap; use std::cmp::Ordering; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::Mutex; use tracing::error; @@ -70,14 +69,12 @@ impl ChainStore { BlocksStateQueryResponse::Error("Invalid message for blocks-state".into()), ))); }; - let Some(delegates) = - query_history.lock().await.current().map(|s| s.delegates.clone()) - else { + let Some(state) = query_history.lock().await.current().map(|s| s.clone()) else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error("unitialised state".to_string()), ))); }; - let res = Self::handle_blocks_query(&query_store, &delegates, &query) + let res = Self::handle_blocks_query(&query_store, &state, &query) .unwrap_or_else(|err| BlocksStateQueryResponse::Error(err.to_string())); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) } @@ -104,7 +101,7 @@ impl ChainStore { }; let mut history = history.lock().await; let mut state = history.get_current_state(); - if !Self::handle_new_params(&mut state.delegates, message).is_ok() { + if !Self::handle_new_params(&mut state, message).is_ok() { return; }; history.commit(block_info.number, state); @@ -129,7 +126,7 @@ impl ChainStore { fn handle_blocks_query( store: &Arc, - delegates: &HashMap, BlockIssuer>, + state: &State, query: &BlocksStateQuery, ) -> Result { match query { @@ -137,7 +134,7 @@ impl ChainStore { let Some(block) = store.get_latest_block()? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, &delegates, true)?; + let info = Self::to_block_info(block, store, &state, true)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order } => { @@ -158,21 +155,21 @@ impl ChainStore { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, &delegates, false)?; + let info = Self::to_block_info(block, store, &state, false)?; Ok(BlocksStateQueryResponse::BlockInfo(info)) } BlocksStateQuery::GetBlockBySlot { slot } => { let Some(block) = store.get_block_by_slot(*slot)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, &delegates, false)?; + let info = Self::to_block_info(block, store, &state, false)?; Ok(BlocksStateQueryResponse::BlockBySlot(info)) } BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { let Some(block) = store.get_block_by_epoch_slot(*epoch, *slot)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let info = Self::to_block_info(block, store, &delegates, false)?; + let info = Self::to_block_info(block, store, &state, false)?; Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) } BlocksStateQuery::GetNextBlocks { @@ -192,7 +189,7 @@ impl ChainStore { let min_number = number + 1 + skip; let max_number = min_number + limit - 1; let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, delegates, false)?; + let info = Self::to_block_info_bulk(blocks, store, &state, false)?; Ok(BlocksStateQueryResponse::NextBlocks(NextBlocks { blocks: info, })) @@ -218,7 +215,7 @@ impl ChainStore { }; let min_number = max_number.saturating_sub(limit - 1); let blocks = store.get_blocks_by_number_range(min_number, max_number)?; - let info = Self::to_block_info_bulk(blocks, store, delegates, false)?; + let info = Self::to_block_info_bulk(blocks, store, &state, false)?; Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { blocks: info, })) @@ -275,18 +272,18 @@ impl ChainStore { fn to_block_info( block: Block, store: &Arc, - delegates: &HashMap, BlockIssuer>, + state: &State, is_latest: bool, ) -> Result { let blocks = vec![block]; - let mut info = Self::to_block_info_bulk(blocks, store, &delegates, is_latest)?; + let mut info = Self::to_block_info_bulk(blocks, store, &state, is_latest)?; Ok(info.remove(0)) } fn to_block_info_bulk( blocks: Vec, store: &Arc, - delegates: &HashMap, BlockIssuer>, + state: &State, final_block_is_latest: bool, ) -> Result> { if blocks.is_empty() { @@ -361,9 +358,11 @@ impl ChainStore { slot: header.slot(), epoch: block.extra.epoch, epoch_slot: block.extra.epoch_slot, - issuer: header - .issuer_vkey() - .map(|key| Self::vkey_to_block_issuer(key.to_vec(), delegates)), + issuer: map_to_block_issuer( + &header, + &state.byron_heavy_delegates, + &state.shelley_genesis_delegates, + ), size: block.bytes.len() as u64, tx_count: decoded.tx_count() as u64, output, @@ -463,48 +462,34 @@ impl ChainStore { Ok(BlockInvolvedAddresses { addresses }) } - fn handle_new_params( - delegates: &mut HashMap, BlockIssuer>, - message: Arc, - ) -> Result<()> { + fn handle_new_params(state: &mut State, message: Arc) -> Result<()> { match message.as_ref() { - Message::Cardano((block_info, CardanoMessage::ProtocolParams(params))) => { + Message::Cardano((_, CardanoMessage::ProtocolParams(params))) => { if let Some(byron) = ¶ms.params.byron { - for (k, deleg) in &byron.heavy_delegation { - delegates.insert(k.clone(), BlockIssuer::HeavyDelegate(deleg.clone())); - } + state.byron_heavy_delegates = byron.heavy_delegation.clone(); } if let Some(shelley) = ¶ms.params.shelley { - for (k, deleg) in &shelley.gen_delegs { - delegates.insert(k.clone(), BlockIssuer::GenesisDelegate(deleg.clone())); - } + state.shelley_genesis_delegates = shelley.gen_delegs.clone(); } } _ => (), } Ok(()) } - - fn vkey_to_block_issuer( - vkey: Vec, - delegates: &HashMap, BlockIssuer>, - ) -> BlockIssuer { - match delegates.get(&vkey) { - Some(delegate) => delegate.clone(), - None => BlockIssuer::SPO(vkey), - } - } } #[derive(Default, Debug, Clone)] pub struct State { - pub delegates: HashMap, BlockIssuer>, + // Keyed on cert + pub byron_heavy_delegates: HashMap, HeavyDelegate>, + pub shelley_genesis_delegates: HashMap, GenesisDelegate>, } impl State { pub fn new() -> Self { Self { - delegates: HashMap::new(), + byron_heavy_delegates: HashMap::new(), + shelley_genesis_delegates: HashMap::new(), } } } diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 9a83f759..3c448c11 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -8,7 +8,7 @@ genesis-key = "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c323 # Download max age in hours. E.g. 8 means 8 hours (if there isn't any snapshot within this time range download from Mithril) download-max-age = "never" # Pause constraint E.g. "epoch:100", "block:1200" -pause = "none" +pause = "epoch:209" [module.upstream-chain-fetcher] sync-point = "snapshot" From e1c5d24cfa3971294acd4237e701ff42ca67f1fa Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 15 Oct 2025 11:02:19 +0100 Subject: [PATCH 27/44] Add clear-on-start option to chain store module --- modules/chain_store/src/stores/fjall.rs | 10 ++++++++-- processes/omnibus/omnibus.toml | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index ebb09ffb..fb28dda9 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,4 +1,4 @@ -use std::{path::Path, sync::Arc}; +use std::{fs, path::Path, sync::Arc}; use acropolis_common::{BlockInfo, TxHash}; use anyhow::Result; @@ -14,6 +14,7 @@ pub struct FjallStore { } const DEFAULT_DATABASE_PATH: &str = "fjall-blocks"; +const DEFAULT_CLEAR_ON_START: bool = true; const BLOCKS_PARTITION: &str = "blocks"; const BLOCK_HASHES_BY_SLOT_PARTITION: &str = "block-hashes-by-slot"; const BLOCK_HASHES_BY_NUMBER_PARTITION: &str = "block-hashes-by-number"; @@ -23,7 +24,12 @@ const TXS_PARTITION: &str = "txs"; impl FjallStore { pub fn new(config: Arc) -> Result { let path = config.get_string("database-path").unwrap_or(DEFAULT_DATABASE_PATH.to_string()); - let fjall_config = fjall::Config::new(Path::new(&path)); + let clear = config.get_bool("clear-on-start").unwrap_or(DEFAULT_CLEAR_ON_START); + let path = Path::new(&path); + if clear && path.exists() { + fs::remove_dir_all(path)?; + } + let fjall_config = fjall::Config::new(path); let keyspace = fjall_config.open()?; let blocks = FjallBlockStore::new(&keyspace)?; let txs = FjallTXStore::new(&keyspace)?; diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 3c448c11..05c5ccc4 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -99,6 +99,8 @@ store-addresses = false index-by-policy = false [module.chain-store] +# Clear state on start up (default true) +clear-on-start = true [module.clock] From 368f201700397ad3584096d0ab3b0e13c4f537e8 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 15 Oct 2025 14:34:25 +0100 Subject: [PATCH 28/44] Create a type for VRFKey - also add some macros to help make these types easily --- common/src/queries/blocks.rs | 12 +-- common/src/serialization.rs | 7 ++ common/src/types.rs | 128 ++++++++++++------------- modules/chain_store/src/chain_store.rs | 4 +- 4 files changed, 76 insertions(+), 75 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 802e1c64..d1cd718b 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,6 +1,7 @@ use crate::{ - queries::misc::Order, serialization::Bech32WithHrp, Address, BlockHash, GenesisDelegate, - HeavyDelegate, KeyHash, TxHash, + queries::misc::Order, + serialization::{Bech32Conversion, Bech32WithHrp}, + Address, BlockHash, GenesisDelegate, HeavyDelegate, KeyHash, TxHash, VRFKey, }; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; @@ -104,8 +105,7 @@ pub struct BlockInfo { pub tx_count: u64, pub output: Option, pub fees: Option, - // TODO: make a proper type for these pub keys - pub block_vrf: Option>, + pub block_vrf: Option, pub op_cert: Option, pub op_cert_counter: Option, pub previous_block: Option, @@ -146,9 +146,7 @@ impl Serialize for BlockInfo { state.serialize_field("fees", &self.fees)?; state.serialize_field( "block_vrf", - &self.block_vrf.clone().map(|vkey| -> String { - vkey.to_bech32_with_hrp("vrf_vk").unwrap_or(String::new()) - }), + &self.block_vrf.clone().and_then(|vkey| vkey.to_bech32().ok()), )?; state.serialize_field("op_cert", &self.op_cert.clone().map(|v| hex::encode(v)))?; state.serialize_field("op_cert_counter", &self.op_cert_counter)?; diff --git a/common/src/serialization.rs b/common/src/serialization.rs index 94f44510..261b058c 100644 --- a/common/src/serialization.rs +++ b/common/src/serialization.rs @@ -26,6 +26,13 @@ where } } +pub trait Bech32Conversion { + fn to_bech32(&self) -> Result; + fn from_bech32(s: &str) -> Result + where + Self: Sized; +} + pub trait Bech32WithHrp { fn to_bech32_with_hrp(&self, hrp: &str) -> Result; fn from_bech32_with_hrp(s: &str, expected_hrp: &str) -> Result, anyhow::Error>; diff --git a/common/src/types.rs b/common/src/types.rs index bcfbe0b1..87dec7e6 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -6,6 +6,7 @@ use crate::{ address::{Address, ShelleyAddress, StakeAddress}, protocol_params, rational_number::RationalNumber, + serialization::{Bech32Conversion, Bech32WithHrp}, }; use anyhow::{anyhow, bail, Error, Result}; use bech32::{Bech32, Hrp}; @@ -91,83 +92,78 @@ pub enum BlockStatus { RolledBack, // Volatile, restarted after rollback } -/// Block hash -#[serde_as] -#[derive( - Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, -)] -pub struct BlockHash(#[serde_as(as = "Hex")] pub [u8; 32]); - -impl TryFrom> for BlockHash { - type Error = Vec; - - fn try_from(vec: Vec) -> Result { - Ok(BlockHash(vec.try_into()?)) - } -} - -impl TryFrom<&[u8]> for BlockHash { - type Error = std::array::TryFromSliceError; - - fn try_from(arr: &[u8]) -> Result { - Ok(BlockHash(arr.try_into()?)) - } -} +macro_rules! declare_byte_array_type { + ($name:ident, $size:expr) => { + /// $name + #[serde_as] + #[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, + )] + pub struct $name(#[serde_as(as = "Hex")] pub [u8; $size]); -impl AsRef<[u8]> for BlockHash { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} + impl From<[u8; $size]> for $name { + fn from(bytes: [u8; $size]) -> Self { + Self(bytes) + } + } -impl Deref for BlockHash { - type Target = [u8; 32]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} + impl TryFrom> for $name { + type Error = Vec; + fn try_from(vec: Vec) -> Result { + Ok($name(vec.try_into()?)) + } + } -/// Transaction hash -#[serde_as] -#[derive( - Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, -)] -pub struct TxHash(#[serde_as(as = "Hex")] pub [u8; 32]); + impl TryFrom<&[u8]> for $name { + type Error = std::array::TryFromSliceError; + fn try_from(arr: &[u8]) -> Result { + Ok($name(arr.try_into()?)) + } + } -impl TryFrom> for TxHash { - type Error = Vec; + impl AsRef<[u8]> for $name { + fn as_ref(&self) -> &[u8] { + &self.0 + } + } - fn try_from(vec: Vec) -> Result { - Ok(TxHash(vec.try_into()?)) - } + impl Deref for $name { + type Target = [u8; $size]; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + }; } -impl TryFrom<&[u8]> for TxHash { - type Error = std::array::TryFromSliceError; - - fn try_from(arr: &[u8]) -> Result { - Ok(TxHash(arr.try_into()?)) - } +macro_rules! declare_byte_array_type_with_bech32 { + ($name:ident, $size:expr, $hrp:expr) => { + declare_byte_array_type!($name, $size); + impl Bech32Conversion for $name { + fn to_bech32(&self) -> Result { + self.0.to_vec().to_bech32_with_hrp($hrp) + } + fn from_bech32(s: &str) -> Result { + match Vec::::from_bech32_with_hrp(s, $hrp) { + Ok(v) => match Self::try_from(v) { + Ok(s) => Ok(s), + Err(_) => Err(Error::msg(format!( + "Bad vector input to {}", + stringify!($name) + ))), + }, + Err(e) => Err(e), + } + } + } + }; } -impl From<[u8; 32]> for TxHash { - fn from(arr: [u8; 32]) -> Self { - TxHash(arr) - } -} +declare_byte_array_type!(BlockHash, 32); -impl AsRef<[u8]> for TxHash { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} +declare_byte_array_type!(TxHash, 32); -impl Deref for TxHash { - type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +declare_byte_array_type_with_bech32!(VRFKey, 32, "vrf_vk"); /// Block info, shared across multiple messages #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 857a8fbb..a9e0572e 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -11,7 +11,7 @@ use acropolis_common::{ }, queries::misc::Order, state_history::{StateHistory, StateHistoryStore}, - Address, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, + Address, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, VRFKey, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -367,7 +367,7 @@ impl ChainStore { tx_count: decoded.tx_count() as u64, output, fees, - block_vrf: header.vrf_vkey().map(|key| key.to_vec()), + block_vrf: header.vrf_vkey().map(|key| VRFKey::try_from(key).ok().unwrap()), op_cert, op_cert_counter, previous_block: header.previous_hash().map(|h| BlockHash(*h)), From 575d094a5bf0cc0fa97f347d9c85f6cbb23ca83a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 15 Oct 2025 19:05:19 +0100 Subject: [PATCH 29/44] Add lookups for block hashes and tx hashes --- common/src/queries/blocks.rs | 21 +++++++++- modules/chain_store/src/chain_store.rs | 53 ++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index d1cd718b..9128e3fb 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,11 +1,12 @@ use crate::{ queries::misc::Order, serialization::{Bech32Conversion, Bech32WithHrp}, - Address, BlockHash, GenesisDelegate, HeavyDelegate, KeyHash, TxHash, VRFKey, + Address, BlockHash, GenesisDelegate, HeavyDelegate, KeyHash, TxHash, TxIdentifier, VRFKey, }; use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; +use std::collections::HashMap; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -60,6 +61,12 @@ pub enum BlocksStateQuery { limit: u64, skip: u64, }, + GetBlockHashes { + block_numbers: Vec, + }, + GetTransactionHashes { + tx_ids: Vec, + }, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -81,6 +88,8 @@ pub enum BlocksStateQueryResponse { BlockTransactions(BlockTransactions), BlockTransactionsCBOR(BlockTransactionsCBOR), BlockInvolvedAddresses(BlockInvolvedAddresses), + BlockHashes(BlockHashes), + TransactionHashes(TransactionHashes), NotFound, Error(String), } @@ -210,3 +219,13 @@ impl Serialize for BlockInvolvedAddress { state.end() } } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BlockHashes { + pub block_hashes: HashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionHashes { + pub tx_hashes: HashMap, +} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index a9e0572e..3363000f 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -5,9 +5,10 @@ use acropolis_common::{ crypto::keyhash, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::blocks::{ - BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, BlockTransaction, - BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, BlocksStateQueryResponse, - NextBlocks, PreviousBlocks, DEFAULT_BLOCKS_QUERY_TOPIC, + BlockHashes, BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, + BlockTransaction, BlockTransactions, BlockTransactionsCBOR, BlocksStateQuery, + BlocksStateQueryResponse, NextBlocks, PreviousBlocks, TransactionHashes, + DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, state_history::{StateHistory, StateHistoryStore}, @@ -255,6 +256,40 @@ impl ChainStore { let addresses = Self::to_block_involved_addresses(block, limit, skip)?; Ok(BlocksStateQueryResponse::BlockInvolvedAddresses(addresses)) } + BlocksStateQuery::GetBlockHashes { block_numbers } => { + let mut block_hashes = HashMap::new(); + for block_number in block_numbers { + if let Ok(Some(block)) = store.get_block_by_number(*block_number) { + if let Ok(hash) = Self::get_block_hash(&block) { + block_hashes.insert(*block_number, hash); + } + } + } + Ok(BlocksStateQueryResponse::BlockHashes(BlockHashes { + block_hashes, + })) + } + BlocksStateQuery::GetTransactionHashes { tx_ids } => { + let mut block_ids: HashMap<_, Vec<_>> = HashMap::new(); + for tx_id in tx_ids { + block_ids.entry(tx_id.block_number()).or_default().push(tx_id); + } + let mut tx_hashes = HashMap::new(); + for (block_number, tx_ids) in block_ids { + if let Ok(Some(block)) = store.get_block_by_number(block_number.into()) { + for tx_id in tx_ids { + if let Ok(hashes) = Self::to_block_transaction_hashes(&block) { + if let Some(hash) = hashes.get(tx_id.tx_index() as usize) { + tx_hashes.insert(*tx_id, *hash); + } + } + } + } + } + Ok(BlocksStateQueryResponse::TransactionHashes( + TransactionHashes { tx_hashes }, + )) + } } } @@ -269,6 +304,12 @@ impl ChainStore { Ok(pallas_traverse::MultiEraBlock::decode(&block.bytes)?.number()) } + fn get_block_hash(block: &Block) -> Result { + Ok(BlockHash( + *pallas_traverse::MultiEraBlock::decode(&block.bytes)?.hash(), + )) + } + fn to_block_info( block: Block, store: &Arc, @@ -382,6 +423,12 @@ impl ChainStore { Ok(block_info) } + fn to_block_transaction_hashes(block: &Block) -> Result> { + let decoded = pallas_traverse::MultiEraBlock::decode(&block.bytes)?; + let txs = decoded.txs(); + Ok(txs.iter().map(|tx| TxHash(*tx.hash())).collect()) + } + fn to_block_transactions( block: Block, limit: &u64, From b24bd0f059f2f180e069f2f5e60a23cae4408843 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 10:18:33 +0100 Subject: [PATCH 30/44] Restore setting --- processes/omnibus/omnibus.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index e53efbd0..93097705 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -8,7 +8,7 @@ genesis-key = "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c323 # Download max age in hours. E.g. 8 means 8 hours (if there isn't any snapshot within this time range download from Mithril) download-max-age = "never" # Pause constraint E.g. "epoch:100", "block:1200" -pause = "epoch:209" +pause = "none" [module.upstream-chain-fetcher] sync-point = "snapshot" From 1e6c620f9cd63ebbec267bc50dad28380bab82d6 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 10:20:00 +0100 Subject: [PATCH 31/44] Move codec lib to top --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0e87b690..ccf0d323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ [workspace] members = [ # Global message and common definitions + "codec", "common", # Modules @@ -26,7 +27,7 @@ members = [ # Process builds "processes/omnibus", # All-inclusive omnibus process "processes/replayer", # All-inclusive process to replay messages - "processes/golden_tests", "codec", #All-inclusive golden tests process + "processes/golden_tests", # All-inclusive golden tests process ] resolver = "2" From 8daf1d8ad0700914330f768d357ca2aace5f8d7b Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 10:38:52 +0100 Subject: [PATCH 32/44] Use crypto::keyhash_244 instead of cryptoxide --- Cargo.lock | 1 - codec/Cargo.toml | 1 - codec/src/block.rs | 9 ++++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58a2d52a..884bbd09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,6 @@ version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", - "cryptoxide 0.5.1", "pallas 0.33.0", "pallas-primitives 0.33.0", "pallas-traverse 0.33.0", diff --git a/codec/Cargo.toml b/codec/Cargo.toml index 9cb1e77a..e80c45d2 100644 --- a/codec/Cargo.toml +++ b/codec/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" acropolis_common = { path = "../common" } anyhow = { workspace = true } -cryptoxide = "0.5.1" pallas = { workspace = true } pallas-primitives = { workspace = true } pallas-traverse = { workspace = true } diff --git a/codec/src/block.rs b/codec/src/block.rs index 9b5e3888..b898534c 100644 --- a/codec/src/block.rs +++ b/codec/src/block.rs @@ -1,5 +1,6 @@ -use acropolis_common::{GenesisDelegate, HeavyDelegate, queries::blocks::BlockIssuer}; -use cryptoxide::hashing::blake2b::Blake2b; +use acropolis_common::{ + GenesisDelegate, HeavyDelegate, crypto::keyhash_224, queries::blocks::BlockIssuer, +}; use pallas_primitives::byron::BlockSig::DlgSig; use pallas_traverse::MultiEraHeader; use std::collections::HashMap; @@ -12,9 +13,7 @@ pub fn map_to_block_issuer( match header.issuer_vkey() { Some(vkey) => match header { MultiEraHeader::ShelleyCompatible(_) => { - let mut context = Blake2b::<224>::new(); - context.update_mut(&vkey); - let digest = context.finalize().as_slice().to_owned(); + let digest = keyhash_224(vkey); if let Some(issuer) = shelley_genesis_delegates .values() .find(|v| v.delegate == digest) From cec73953e2ef47aaca0d599b594a59539fa689f4 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 10:55:32 +0100 Subject: [PATCH 33/44] Make import of GenesisDelegate consistent with other types imports --- modules/accounts_state/src/monetary.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/accounts_state/src/monetary.rs b/modules/accounts_state/src/monetary.rs index 8278aaea..bb3c9485 100644 --- a/modules/accounts_state/src/monetary.rs +++ b/modules/accounts_state/src/monetary.rs @@ -111,7 +111,7 @@ mod tests { use acropolis_common::NetworkId; use acropolis_common::{ protocol_params::{Nonce, NonceVariant, ProtocolVersion, ShelleyProtocolParams}, - types::GenesisDelegate, + GenesisDelegate, }; use chrono::{DateTime, Utc}; use std::collections::HashMap; From 251fd26ae5580e7c0632b3984d576b2fa7db9cb0 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 11:18:42 +0100 Subject: [PATCH 34/44] Add comments for params message reading --- modules/chain_store/src/chain_store.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 5b26d932..bf1fd32a 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -84,6 +84,8 @@ impl ChainStore { let mut new_blocks_subscription = context.subscribe(&new_blocks_topic).await?; let mut params_subscription = context.subscribe(¶ms_topic).await?; context.run(async move { + // Get promise of params message so the params queue is cleared and + // the message is ready as soon as possible when we need it let mut params_message = params_subscription.read(); loop { let Ok((_, message)) = new_blocks_subscription.read().await else { @@ -106,6 +108,7 @@ impl ChainStore { return; }; history.commit(block_info.number, state); + // Have the next params message ready for the next epoch params_message = params_subscription.read(); } } From ab113f2f649b8d8a2890d0273db3f2dcbd8cb9fb Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 11:38:54 +0100 Subject: [PATCH 35/44] Get block number from key if possible --- modules/chain_store/src/chain_store.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index bf1fd32a..37e6effb 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -189,7 +189,10 @@ impl ChainStore { let Some(block) = Self::get_block_by_key(store, &block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let number = Self::get_block_number(&block)?; + let number = match block_key { + BlockKey::Number(number) => *number, + _ => Self::get_block_number(&block)? + }; let min_number = number + 1 + skip; let max_number = min_number + limit - 1; let blocks = store.get_blocks_by_number_range(min_number, max_number)?; @@ -211,7 +214,10 @@ impl ChainStore { let Some(block) = Self::get_block_by_key(store, &block_key)? else { return Ok(BlocksStateQueryResponse::NotFound); }; - let number = Self::get_block_number(&block)?; + let number = match block_key { + BlockKey::Number(number) => *number, + _ => Self::get_block_number(&block)? + }; let Some(max_number) = number.checked_sub(1 + skip) else { return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { blocks: vec![], From 80d42cef34302e3a6ea487d785bc66f4bb93d640 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 12:52:16 +0100 Subject: [PATCH 36/44] Tidy up redundant code --- modules/chain_store/src/stores/fjall.rs | 29 +++++++------------------ 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index fb28dda9..ce81baae 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -57,11 +57,11 @@ impl super::Store for FjallStore { let mut batch = self.keyspace.batch(); self.blocks.insert(&mut batch, info, &raw); for (index, hash) in tx_hashes.iter().enumerate() { - let tx_ref = StoredTransaction::BlockReference { + let block_ref = BlockReference { block_hash: info.hash.to_vec(), index, }; - self.txs.insert_tx(&mut batch, *hash, tx_ref); + self.txs.insert_tx(&mut batch, *hash, block_ref); } batch.commit()?; @@ -173,14 +173,9 @@ impl FjallBlockStore { fn get_by_number_range(&self, min_number: u64, max_number: u64) -> Result> { let min_number_bytes = min_number.to_be_bytes(); let max_number_bytes = max_number.to_be_bytes(); - let mut hashes = vec![]; + let mut blocks = vec![]; for res in self.block_hashes_by_number.range(min_number_bytes..=max_number_bytes) { let (_, hash) = res?; - hashes.push(hash); - } - - let mut blocks = vec![]; - for hash in hashes { if let Some(block) = self.get_by_hash(&hash)? { blocks.push(block); } @@ -221,26 +216,18 @@ impl FjallTXStore { Ok(Self { txs }) } - fn insert_tx(&self, batch: &mut Batch, hash: TxHash, tx: StoredTransaction) { - let bytes = minicbor::to_vec(tx).expect("infallible"); + fn insert_tx(&self, batch: &mut Batch, hash: TxHash, block_ref: BlockReference) { + let bytes = minicbor::to_vec(block_ref).expect("infallible"); batch.insert(&self.txs, hash.as_ref(), bytes); } } #[derive(minicbor::Decode, minicbor::Encode)] -enum StoredTransaction { +struct BlockReference { #[n(0)] - BlockReference { - #[n(0)] - block_hash: Vec, - #[n(1)] - index: usize, - }, + block_hash: Vec, #[n(1)] - Inline { - #[n(0)] - bytes: Vec, - }, + index: usize, } #[cfg(test)] From 70005d526a0cd8bb5b2d746d1a507206ce923a3b Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 13:57:20 +0100 Subject: [PATCH 37/44] Add unit test for get_blocks_by_number_range --- modules/chain_store/src/stores/fjall.rs | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index ce81baae..5dc32e37 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -241,6 +241,19 @@ mod tests { const TEST_BLOCK: &str = "820785828a1a0010afaa1a0150d7925820a22f65265e7a71cfc3b637d6aefe8f8241d562f5b1b787ff36697ae4c3886f185820e856c84a3d90c8526891bd58d957afadc522de37b14ae04c395db8a7a1b08c4a582015587d5633be324f8de97168399ab59d7113f0a74bc7412b81f7cc1007491671825840af9ff8cb146880eba1b12beb72d86be46fbc98f6b88110cd009bd6746d255a14bb0637e3a29b7204bff28236c1b9f73e501fed1eb5634bd741be120332d25e5e5850a9f1de24d01ba43b025a3351b25de50cc77f931ed8cdd0be632ad1a437ec9cf327b24eb976f91dbf68526f15bacdf8f0c1ea4a2072df9412796b34836a816760f4909b98c0e76b160d9aec6b2da060071903705820b5858c659096fcc19f2f3baef5fdd6198641a623bd43e792157b5ea3a2ecc85c8458200ca1ec2c1c2af308bd9e7a86eb12d603a26157752f3f71c337781c456e6ed0c90018a558408e554b644a2b25cb5892d07a26c273893829f1650ec33bf6809d953451c519c32cfd48d044cd897a17cdef154d5f5c9b618d9b54f8c49e170082c08c236524098209005901c05a96b747789ef6678b2f4a2a7caca92e270f736e9b621686f95dd1332005102faee21ed50cf6fa6c67e38b33df686c79c91d55f30769f7c964d98aa84cbefe0a808ee6f45faaf9badcc3f746e6a51df1aa979195871fd5ffd91037ea216803be7e7fccbf4c13038c459c7a14906ab57f3306fe155af7877c88866eede7935f642f6a72f1368c33ed5cc7607c995754af787a5af486958edb531c0ae65ce9fdce423ad88925e13ef78700950093ae707bb1100299a66a5bb15137f7ba62132ba1c9b74495aac50e1106bacb5db2bed4592f66b610c2547f485d061c6c149322b0c92bdde644eb672267fdab5533157ff398b9e16dd6a06edfd67151e18a3ac93fc28a51f9a73f8b867f5f432b1d9b5ae454ef63dea7e1a78631cf3fee1ba82db61726701ac5db1c4fee4bb6316768c82c0cdc4ebd58ccc686be882f9608592b3c718e4b5d356982a6b83433fe76d37394eff9f3a8e4773e3bab9a8b93b4ea90fa33bfbcf0dc5a21bfe64be2eefaa82c0494ab729e50596110f60ae9ad64b3eb9ddb54001b03cc264b65634c071d3b24a44322f39a9eae239fd886db8d429969433cb2d0a82d7877f174b0e154262f1af44ce5bc053b62daadd2926f957440ff3981a600d9010281825820af09d312a642fecb47da719156517bec678469c15789bcf002ce2ef563edf54200018182581d6052e63f22c5107ed776b70f7b92248b02552fd08f3e747bc745099441821b00000001373049f4a1581c34250edd1e9836f5378702fbf9416b709bc140e04f668cc355208518a1494154414441636f696e1953a6021a000306b5031a01525e0209a1581c34250edd1e9836f5378702fbf9416b709bc140e04f668cc355208518a1494154414441636f696e010758206cf243cc513691d9edc092b1030c6d1e5f9a8621a4d4383032b3d292d4679d5c81a200d90102828258201287e9ce9e00a603d250b557146aa0581fc4edf277a244ce39d3b2f2ced5072f5840d40fbe736892d8dab09e864a25f2e59fb7bfe445d960bbace30996965dc12a34c59746febf9d32ade65b6a9e1a1a6efc53830a3acaab699972cd4f240c024c0f825820742d8af3543349b5b18f3cba28f23b2d6e465b9c136c42e1fae6b2390f565427584005637b5645784bd998bb8ed837021d520200211fdd958b9a4d4b3af128fa6e695fb86abad7a9ddad6f1db946f8b812113fa16cfb7025e2397277b14e8c9bed0a01d90102818200581c45d70e54f3b5e9c5a2b0cd417028197bd6f5fa5378c2f5eba896678da100d90103a100a11902a2a1636d73678f78264175746f2d4c6f6f702d5472616e73616374696f6e202336323733363820627920415441444160783c4c6976652045706f6368203235352c207765206861766520303131682035396d20323573206c65667420756e74696c20746865206e657874206f6e6578344974277320536f6e6e746167202d20323520466562727561722032303234202d2031333a33303a333520696e20417573747269616060607820412072616e646f6d205a656e2d51756f746520666f7220796f753a20f09f998f78344974206973206e6576657220746f6f206c61746520746f206265207768617420796f75206d696768742068617665206265656e2e6f202d2047656f72676520456c696f746078374e6f64652d5265766973696f6e3a203462623230343864623737643632336565366533363738363138633264386236633436373633333360782953616e63686f4e657420697320617765736f6d652c206861766520736f6d652066756e2120f09f988d7819204265737420726567617264732c204d617274696e203a2d2980"; + // Mainnet blocks 1-9 + const TEST_BLOCKS: [&str; 9] = [ + "820183851a2d964a09582089d9b5a5b8ddc8d7e5a6795e9774d97faf1efea59b2caf7eaf9f8c5b32059df484830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e40988482000058401bc97a2fe02c297880ce8ecfd997fe4c1ec09ee10feeee9f686760166b05281d6283468ffd93becb0c956ccddd642df9b1244c915911185fa49355f6f22bfab98101820282840058401bc97a2fe02c297880ce8ecfd997fe4c1ec09ee10feeee9f686760166b05281d6283468ffd93becb0c956ccddd642df9b1244c915911185fa49355f6f22bfab9584061261a95b7613ee6bf2067dad77b70349729b0c50d57bc1cf30de0db4a1e73a885d0054af7c23fc6c37919dba41c602a57e2d0f9329a7954b867338d6fb2c9455840e03e62f083df5576360e60a32e22bbb07b3c8df4fcab8079f1d6f61af3954d242ba8a06516c395939f24096f3df14e103a7d9c2b80a68a9363cf1f27c7a4e307584044f18ef23db7d2813415cb1b62e8f3ead497f238edf46bb7a97fd8e9105ed9775e8421d18d47e05a2f602b700d932c181e8007bbfb231d6f1a050da4ebeeba048483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a095820f0f7892b5c333cffc4b3c4344de48af4cc63f55e44936196f365a9ef2244134f84830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e409884820001584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf6575281028202828400584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf657525840e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d190dcc246f56c8e33ac0037066e2f664ddaa985ea5284082643308dde4f5bfedf5840c8b39f094dc00608acb2d20ff274cb3e0c022ccb0ce558ea7c1a2d3a32cd54b42cc30d32406bcfbb7f2f86d05d2032848be15b178e3ad776f8b1bc56a671400d5840923c7714af7fe4b1272fc042111ece6fd08f5f16298d62bae755c70c1e1605697cbaed500e196330f40813128250d9ede9c8557b33f48e8a5f32f765929e4a0d8483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a0958201dbc81e3196ba4ab9dcb07e1c37bb28ae1c289c0707061f28b567c2f48698d5084830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e409884820002584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf6575281038202828400584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf657525840e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d190dcc246f56c8e33ac0037066e2f664ddaa985ea5284082643308dde4f5bfedf5840c8b39f094dc00608acb2d20ff274cb3e0c022ccb0ce558ea7c1a2d3a32cd54b42cc30d32406bcfbb7f2f86d05d2032848be15b178e3ad776f8b1bc56a671400d584094966ae05c576724fd892aa91959fc191833fade8e118c36a12eb453003b634ccc9bb7808bcf950c5da9145cffad9e26061bfe9853817706008f75a464c814038483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a09582052b7912de176ab76c233d6e08ccdece53ac1863c08cc59d3c5dec8d924d9b53684830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e409884820003584026566e86fc6b9b177c8480e275b2b112b573f6d073f9deea53b8d99c4ed976b335b2b3842f0e380001f090bc923caa9691ed9115e286da9421e2745c7acc87f181048202828400584026566e86fc6b9b177c8480e275b2b112b573f6d073f9deea53b8d99c4ed976b335b2b3842f0e380001f090bc923caa9691ed9115e286da9421e2745c7acc87f15840f14f712dc600d793052d4842d50cefa4e65884ea6cf83707079eb8ce302efc85dae922d5eb3838d2b91784f04824d26767bfb65bd36a36e74fec46d09d98858d58408ab43e904b06e799c1817c5ced4f3a7bbe15cdbf422dea9d2d5dc2c6105ce2f4d4c71e5d4779f6c44b770a133636109949e1f7786acb5a732bcdea0470fea4065840273c97ffc6e16c86772bdb9cb52bfe99585917f901ee90ce337a9654198fb09ca6bc51d74a492261c169ca5a196a04938c740ba6629254fe566a590370cc9b0f8483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a095820be06c81f4ad34d98578b67840d8e65b2aeb148469b290f6b5235e41b75d3857284830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e4098848200045840d2965c869901231798c5d02d39fca2a79aa47c3e854921b5855c82fd1470891517e1fa771655ec8cad13ecf6e5719adc5392fc057e1703d5f583311e837462f1810582028284005840d2965c869901231798c5d02d39fca2a79aa47c3e854921b5855c82fd1470891517e1fa771655ec8cad13ecf6e5719adc5392fc057e1703d5f583311e837462f158409180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda81344440c6ad0e5306fd035bef9281da5d8fbd38f59f588f7081016ee61113d25840cf6ddc111545f61c2442b68bd7864ea952c428d145438948ef48a4af7e3f49b175564007685be5ae3c9ece0ab27de09721db0cb63aa67dc081a9f82d7e84210d58409f9649c57d902a9fe94208b40eb31ffb4d703e5692c16bcd3a4370b448b4597edaa66f3e4f3bd5858d8e6a57cc0734ec04174d13cbc62eabe64af49271245f068483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a09582046debe49b4fe0bc8c07cfe650de89632ca1ab5d58f04f8c88d8102da7ef79b7f84830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e4098848200055840993a8f056d2d3e50b0ac60139f10df8f8123d5f7c4817b40dac2b5dd8aa94a82e8536832e6312ddfc0787d7b5310c815655ada4fdbcf6b12297d4458eccc2dfb810682028284005840993a8f056d2d3e50b0ac60139f10df8f8123d5f7c4817b40dac2b5dd8aa94a82e8536832e6312ddfc0787d7b5310c815655ada4fdbcf6b12297d4458eccc2dfb584089c29f8c4af27b7accbe589747820134ebbaa1caf3ce949270a3d0c7dcfd541b1def326d2ef0db780341c9e261f04890cdeef1f9c99f6d90b8edca7d3cfc09885840496b29b5c57e8ac7cffc6e8b5e40b3d260e407ad4d09792decb0a22d54da7f8828265688a18aa1a5c76d9e7477a5f4a650501409fdcd3855b300fd2e2bc3c6055840b3bea437aa37a2abdc1a35d9ff01cddb387c543d8034c565dc18525ccd16a0f761d3556d8b90add263db77ee6200aebd6ec2fcc2ec20153f9227b07053a7a50a8483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a095820365201e928da50760fce4bdad09a7338ba43a43aff1c0e8d3ec458388c932ec884830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e409884820006584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf6575281078202828400584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf657525840e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d190dcc246f56c8e33ac0037066e2f664ddaa985ea5284082643308dde4f5bfedf5840c8b39f094dc00608acb2d20ff274cb3e0c022ccb0ce558ea7c1a2d3a32cd54b42cc30d32406bcfbb7f2f86d05d2032848be15b178e3ad776f8b1bc56a671400d584077ddc2fe0557a5c0454a7af6f29e39e603907b927aeeab23e18abe0022cf219197a9a359ab07986a6b42a6e970139edd4a36555661274ae3ac27d4e7c509790e8483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a095820e39d988dd815fc2cb234c2abef0d7f57765eeffb67331814bdb01c590359325e84830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e409884820007584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf6575281088202828400584050733161fdafb6c8cb6fae0e25bdf9555105b3678efb08f1775b9e90de4f5c77bcc8cefff8d9011cb278b28fddc86d9bab099656d77a7856c7619108cbf657525840e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d190dcc246f56c8e33ac0037066e2f664ddaa985ea5284082643308dde4f5bfedf5840c8b39f094dc00608acb2d20ff274cb3e0c022ccb0ce558ea7c1a2d3a32cd54b42cc30d32406bcfbb7f2f86d05d2032848be15b178e3ad776f8b1bc56a671400d58405b2f5d0f55ec53bf74a09e2154f7ad56f437a1a9198041e3ec96f5f17a0cfa8c7d71a7871efabd990184b5166b2ac83af0b63bb727fd7157541db7a232ffdc048483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + "820183851a2d964a0958202d9136c363c69ad07e1a918de2ff5aeeba4361e33b9c2597511874f211ca26e984830058200e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a85820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b8300582025777aca9e4a73d48fc73b4f961d345b06d4a6f349cb7916570d35537d53479f5820d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c5820afc0da64183bf2664f3d4eec7238d524ba607faeeab24fc100eb861dba69971b58204e66280cd94d591072349bec0a3090a53aa945562efb6d08d56e53654b0e4098848200085840d2965c869901231798c5d02d39fca2a79aa47c3e854921b5855c82fd1470891517e1fa771655ec8cad13ecf6e5719adc5392fc057e1703d5f583311e837462f1810982028284005840d2965c869901231798c5d02d39fca2a79aa47c3e854921b5855c82fd1470891517e1fa771655ec8cad13ecf6e5719adc5392fc057e1703d5f583311e837462f158409180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda81344440c6ad0e5306fd035bef9281da5d8fbd38f59f588f7081016ee61113d25840cf6ddc111545f61c2442b68bd7864ea952c428d145438948ef48a4af7e3f49b175564007685be5ae3c9ece0ab27de09721db0cb63aa67dc081a9f82d7e84210d58407b26babee8ad96bf5cdd20cac799ca56c90b6ff9df1f1140f50f021063f719e3791f22be92353a8ae16045b0d52a51c8b1219ce782fd4198cf15b745348021018483000000826a63617264616e6f2d736c00a058204ba92aa320c60acc9ad7b9a64f2eda55c4d2ec28e604faf186708b4f0c4e8edf849fff8300d9010280d90102809fff82809fff81a0", + ]; + fn test_block_info(bytes: &[u8]) -> BlockInfo { let block = MultiEraBlock::decode(bytes).unwrap(); let genesis = GenesisValues::mainnet(); @@ -263,6 +276,10 @@ mod tests { hex::decode(TEST_BLOCK).unwrap() } + fn test_block_range_bytes(count: usize) -> Vec> { + TEST_BLOCKS[0..count].iter().map(|b| hex::decode(b).unwrap()).collect() + } + fn build_block(info: &BlockInfo, bytes: &[u8]) -> Block { let extra = ExtraBlockData { epoch: info.epoch, @@ -336,6 +353,22 @@ mod tests { assert_eq!(block, new_block.unwrap()); } + #[test] + fn should_get_blocks_by_number_range() { + let state = init_state(); + let blocks_bytes = test_block_range_bytes(6); + let mut blocks = Vec::new(); + for bytes in blocks_bytes { + let info = test_block_info(&bytes); + blocks.push(build_block(&info, &bytes)); + state.store.insert_block(&info, &bytes).unwrap(); + } + let new_blocks = state.store.get_blocks_by_number_range(2, 4).unwrap(); + assert_eq!(blocks[1], new_blocks[0]); + assert_eq!(blocks[2], new_blocks[1]); + assert_eq!(blocks[3], new_blocks[2]); + } + #[test] fn should_get_block_by_epoch_slot() { let state = init_state(); From 4c891134ec05c224cf5cc4011be96694770a2a4f Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Oct 2025 18:46:55 +0100 Subject: [PATCH 38/44] Add a helper function for REST queries - rest_query_state is essentially query_state that returns RESTResponse, and handles the common error cases --- common/src/queries/utils.rs | 41 ++- modules/chain_store/src/chain_store.rs | 4 +- .../rest_blockfrost/src/handlers/blocks.rs | 330 ++++-------------- modules/rest_blockfrost/src/types.rs | 2 +- 4 files changed, 108 insertions(+), 269 deletions(-) diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 43ef1932..d385f11c 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -1,8 +1,9 @@ use anyhow::Result; use caryatid_sdk::Context; +use serde::Serialize; use std::sync::Arc; -use crate::messages::Message; +use crate::messages::{Message, RESTResponse}; pub async fn query_state( context: &Arc>, @@ -20,3 +21,41 @@ where Ok(extractor(message)?) } + +/// The outer option in the extractor return value is whether the response was handled by F +pub async fn rest_query_state( + context: &Arc>, + topic: &str, + request_msg: Arc, + extractor: F, +) -> Result +where + F: FnOnce(Message) -> Option, anyhow::Error>>, + T: Serialize, +{ + let result = query_state(&context, topic, request_msg, |response| { + match extractor(response) { + Some(response) => response, + None => Err(anyhow::anyhow!( + "Unexpected response message type while calling {topic}" + )), + } + }) + .await; + match result { + Ok(result) => match result { + Some(result) => match serde_json::to_string(&result) { + Ok(json) => Ok(RESTResponse::with_json(200, &json)), + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while calling {topic}: {e}"), + )), + }, + None => Ok(RESTResponse::with_text(404, "Not found")), + }, + Err(e) => Ok(RESTResponse::with_text( + 500, + &format!("Internal server error while calling {topic}: {e}"), + )), + } +} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 37e6effb..1942bd94 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -191,7 +191,7 @@ impl ChainStore { }; let number = match block_key { BlockKey::Number(number) => *number, - _ => Self::get_block_number(&block)? + _ => Self::get_block_number(&block)?, }; let min_number = number + 1 + skip; let max_number = min_number + limit - 1; @@ -216,7 +216,7 @@ impl ChainStore { }; let number = match block_key { BlockKey::Number(number) => *number, - _ => Self::get_block_number(&block)? + _ => Self::get_block_number(&block)?, }; let Some(max_number) = number.checked_sub(1 + skip) else { return Ok(BlocksStateQueryResponse::PreviousBlocks(PreviousBlocks { diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 22641d53..205fdab5 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -5,11 +5,11 @@ use acropolis_common::{ queries::{ blocks::{BlockKey, BlocksStateQuery, BlocksStateQueryResponse}, misc::Order, - utils::query_state, + utils::rest_query_state, }, BlockHash, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use std::collections::HashMap; use std::sync::Arc; @@ -58,37 +58,21 @@ async fn handle_blocks_latest_blockfrost( let blocks_latest_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlock, ))); - let block_info = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, blocks_latest_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlock(blocks_latest), - )) => Ok(blocks_latest), + )) => Some(Ok(Some(BlockInfoREST(blocks_latest)))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving latest block: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest block" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match serde_json::to_string(&BlockInfoREST(&block_info)) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block info: {e}"), - )), - } + .await } /// Handle `/blocks/{hash_or_number}` @@ -105,43 +89,24 @@ async fn handle_blocks_hash_number_blockfrost( let block_info_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockInfo { block_key }, ))); - let block_info = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_info_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockInfo(block_info), - )) => Ok(Some(block_info)), + )) => Some(Ok(Some(BlockInfoREST(block_info)))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_info { - Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block info: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/latest/txs`, `/blocks/{hash_or_number}/txs` @@ -201,37 +166,21 @@ async fn handle_blocks_latest_transactions_blockfrost( let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order }, ))); - let block_txs = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, blocks_latest_txs_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlockTransactions(blocks_txs), - )) => Ok(blocks_txs), + )) => Some(Ok(Some(blocks_txs))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving latest block transactions: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest block transactions" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match serde_json::to_string(&block_txs) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block transactions: {e}"), - )), - } + .await } /// Handle `/blocks/{hash_or_number}/txs` @@ -256,43 +205,24 @@ async fn handle_blocks_hash_number_transactions_blockfrost( order, }, ))); - let block_txs = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_txs_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockTransactions(block_txs), - )) => Ok(Some(block_txs)), + )) => Some(Ok(Some(block_txs))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block transactions by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block transactions by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_txs { - Some(block_txs) => match serde_json::to_string(&block_txs) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block transactions: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/latest/txs/cbor`, `/blocks/{hash_or_number}/txs/cbor` @@ -352,37 +282,21 @@ async fn handle_blocks_latest_transactions_cbor_blockfrost( let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlockTransactionsCBOR { limit, skip, order }, ))); - let block_txs_cbor = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, blocks_latest_txs_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlockTransactionsCBOR(blocks_txs), - )) => Ok(blocks_txs), + )) => Some(Ok(Some(blocks_txs))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving latest block transactions CBOR: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest block transactions CBOR" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match serde_json::to_string(&block_txs_cbor) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block transactions CBOR: {e}"), - )), - } + .await } /// Handle `/blocks/{hash_or_number}/txs/cbor` @@ -407,43 +321,24 @@ async fn handle_blocks_hash_number_transactions_cbor_blockfrost( order, }, ))); - let block_txs_cbor = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_txs_cbor_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockTransactionsCBOR(block_txs_cbor), - )) => Ok(Some(block_txs_cbor)), + )) => Some(Ok(Some(block_txs_cbor))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block transactions CBOR by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block transactions CBOR by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_txs_cbor { - Some(block_txs_cbor) => match serde_json::to_string(&block_txs_cbor) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block transactions CBOR: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/{hash_or_number}/next` @@ -477,43 +372,24 @@ pub async fn handle_blocks_hash_number_next_blockfrost( skip, }, ))); - let blocks_next = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, blocks_next_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NextBlocks(blocks_next), - )) => Ok(Some(blocks_next)), + )) => Some(Ok(Some(blocks_next))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving next blocks by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving next blocks by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match blocks_next { - Some(blocks_next) => match serde_json::to_string(&blocks_next) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving next blocks: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/{hash_or_number}/previous` @@ -547,43 +423,24 @@ pub async fn handle_blocks_hash_number_previous_blockfrost( skip, }, ))); - let blocks_previous = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, blocks_previous_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::PreviousBlocks(blocks_previous), - )) => Ok(Some(blocks_previous)), + )) => Some(Ok(Some(blocks_previous))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving previous blocks by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving previous blocks by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match blocks_previous { - Some(blocks_previous) => match serde_json::to_string(&blocks_previous) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving previous blocks: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/slot/{slot_number}` @@ -605,43 +462,24 @@ pub async fn handle_blocks_slot_blockfrost( let block_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockBySlot { slot }, ))); - let block_info = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_slot_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockBySlot(block_info), - )) => Ok(Some(block_info)), + )) => Some(Ok(Some(block_info))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block by slot: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block by slot" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_info { - Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block info: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/epoch/{epoch_number}/slot/{slot_number}` @@ -668,43 +506,24 @@ pub async fn handle_blocks_epoch_slot_blockfrost( let block_epoch_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockByEpochSlot { epoch, slot }, ))); - let block_info = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_epoch_slot_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockByEpochSlot(block_info), - )) => Ok(Some(block_info)), + )) => Some(Ok(Some(block_info))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block by epoch slot: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block by epoch slot" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_info { - Some(block_info) => match serde_json::to_string(&BlockInfoREST(&block_info)) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block info: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } /// Handle `/blocks/{hash_or_number}/addresses` @@ -738,41 +557,22 @@ pub async fn handle_blocks_hash_number_addresses_blockfrost( skip, }, ))); - let block_addresses = query_state( + rest_query_state( &context, &handlers_config.blocks_query_topic, block_involved_addresses_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockInvolvedAddresses(block_addresses), - )) => Ok(Some(block_addresses)), + )) => Some(Ok(Some(block_addresses))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NotFound, - )) => Ok(None), + )) => Some(Ok(None)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving block addresses by hash or number: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving block addresses by hash or number" - )) - } + )) => Some(Err(anyhow!(e))), + _ => None, }, ) - .await?; - - match block_addresses { - Some(block_addresses) => match serde_json::to_string(&block_addresses) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving block addresses: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - } + .await } diff --git a/modules/rest_blockfrost/src/types.rs b/modules/rest_blockfrost/src/types.rs index 8e6f28b0..3f505c18 100644 --- a/modules/rest_blockfrost/src/types.rs +++ b/modules/rest_blockfrost/src/types.rs @@ -53,7 +53,7 @@ impl From for EpochActivityRest { // REST response structure for /blocks/latest #[derive(Serialize)] -pub struct BlockInfoREST<'a>(pub &'a BlockInfo); +pub struct BlockInfoREST(pub BlockInfo); // REST response structure for /governance/dreps #[derive(Serialize)] From 6f6ab7bcad0fc87c553839c7c0167775c150bab4 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 10:43:42 +0100 Subject: [PATCH 39/44] Move bech-order address helper to common --- common/src/address.rs | 18 ++++++++++++++++++ modules/chain_store/src/chain_store.rs | 18 +----------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/common/src/address.rs b/common/src/address.rs index 42f8eeda..f121cc20 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -7,6 +7,8 @@ use anyhow::{anyhow, bail, Result}; use crc::{Crc, CRC_32_ISO_HDLC}; use minicbor::data::IanaTag; use serde_with::{hex::Hex, serde_as}; +use std::cmp::Ordering; + /// a Byron-era address #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] @@ -517,6 +519,22 @@ impl Address { } } +/// Used for ordering addresses by their bech representation +#[derive(Eq, PartialEq)] +pub struct BechOrdAddress(pub Address); + +impl Ord for BechOrdAddress { + fn cmp(&self, other: &Self) -> Ordering { + self.0.to_string().into_iter().cmp(other.0.to_string()) + } +} + +impl PartialOrd for BechOrdAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + // -- Tests -- #[cfg(test)] mod tests { diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 1942bd94..06086dde 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -12,12 +12,11 @@ use acropolis_common::{ }, queries::misc::Order, state_history::{StateHistory, StateHistoryStore}, - Address, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, VRFKey, + BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, VRFKey, }; use anyhow::{bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; -use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::Mutex; @@ -549,18 +548,3 @@ impl State { } } } - -#[derive(Eq, PartialEq)] -struct BechOrdAddress(Address); - -impl Ord for BechOrdAddress { - fn cmp(&self, other: &Self) -> Ordering { - self.0.to_string().into_iter().cmp(other.0.to_string()) - } -} - -impl PartialOrd for BechOrdAddress { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} From 669921f3aee6adde4efca085afdbc08c7374087c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 12:02:01 +0100 Subject: [PATCH 40/44] Move byte array types into own mod --- common/src/byte_array.rs | 77 ++++++++++++++++++++++++++++++++++++++++ common/src/lib.rs | 2 ++ common/src/messages.rs | 1 + common/src/types.rs | 77 ++-------------------------------------- 4 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 common/src/byte_array.rs diff --git a/common/src/byte_array.rs b/common/src/byte_array.rs new file mode 100644 index 00000000..b61c4e22 --- /dev/null +++ b/common/src/byte_array.rs @@ -0,0 +1,77 @@ +use crate::serialization::{Bech32Conversion, Bech32WithHrp}; +use anyhow::Error; +use serde_with::{hex::Hex, serde_as}; +use std::ops::Deref; + +macro_rules! declare_byte_array_type { + ($name:ident, $size:expr) => { + /// $name + #[serde_as] + #[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, + )] + pub struct $name(#[serde_as(as = "Hex")] pub [u8; $size]); + + impl From<[u8; $size]> for $name { + fn from(bytes: [u8; $size]) -> Self { + Self(bytes) + } + } + + impl TryFrom> for $name { + type Error = Vec; + fn try_from(vec: Vec) -> Result { + Ok($name(vec.try_into()?)) + } + } + + impl TryFrom<&[u8]> for $name { + type Error = std::array::TryFromSliceError; + fn try_from(arr: &[u8]) -> Result { + Ok($name(arr.try_into()?)) + } + } + + impl AsRef<[u8]> for $name { + fn as_ref(&self) -> &[u8] { + &self.0 + } + } + + impl Deref for $name { + type Target = [u8; $size]; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + }; +} + +macro_rules! declare_byte_array_type_with_bech32 { + ($name:ident, $size:expr, $hrp:expr) => { + declare_byte_array_type!($name, $size); + impl Bech32Conversion for $name { + fn to_bech32(&self) -> Result { + self.0.to_vec().to_bech32_with_hrp($hrp) + } + fn from_bech32(s: &str) -> Result { + match Vec::::from_bech32_with_hrp(s, $hrp) { + Ok(v) => match Self::try_from(v) { + Ok(s) => Ok(s), + Err(_) => Err(Error::msg(format!( + "Bad vector input to {}", + stringify!($name) + ))), + }, + Err(e) => Err(e), + } + } + } + }; +} + +declare_byte_array_type!(BlockHash, 32); + +declare_byte_array_type!(TxHash, 32); + +declare_byte_array_type_with_bech32!(VRFKey, 32, "vrf_vk"); diff --git a/common/src/lib.rs b/common/src/lib.rs index 3d755c25..c2c58283 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,6 +1,7 @@ // Acropolis common library - main library exports pub mod address; +pub mod byte_array; pub mod calculations; pub mod cip19; pub mod crypto; @@ -23,4 +24,5 @@ pub mod types; // Flattened re-exports pub use self::address::*; +pub use self::byte_array::*; pub use self::types::*; diff --git a/common/src/messages.rs b/common/src/messages.rs index e345e488..23dc362c 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -25,6 +25,7 @@ use crate::queries::{ transactions::{TransactionsStateQuery, TransactionsStateQueryResponse}, }; +use crate::byte_array::*; use crate::types::*; // Caryatid core messages which we re-export diff --git a/common/src/types.rs b/common/src/types.rs index dcc19d66..9b1d323f 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -6,7 +6,7 @@ use crate::{ address::{Address, ShelleyAddress, StakeAddress}, protocol_params, rational_number::RationalNumber, - serialization::{Bech32Conversion, Bech32WithHrp}, + BlockHash, TxHash, }; use anyhow::{anyhow, bail, Error, Result}; use bech32::{Bech32, Hrp}; @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{hex::Hex, serde_as}; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; -use std::ops::{AddAssign, Deref, Neg}; +use std::ops::{AddAssign, Neg}; use std::{cmp::Ordering, fmt}; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -104,79 +104,6 @@ pub enum BlockStatus { RolledBack, // Volatile, restarted after rollback } -macro_rules! declare_byte_array_type { - ($name:ident, $size:expr) => { - /// $name - #[serde_as] - #[derive( - Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, - )] - pub struct $name(#[serde_as(as = "Hex")] pub [u8; $size]); - - impl From<[u8; $size]> for $name { - fn from(bytes: [u8; $size]) -> Self { - Self(bytes) - } - } - - impl TryFrom> for $name { - type Error = Vec; - fn try_from(vec: Vec) -> Result { - Ok($name(vec.try_into()?)) - } - } - - impl TryFrom<&[u8]> for $name { - type Error = std::array::TryFromSliceError; - fn try_from(arr: &[u8]) -> Result { - Ok($name(arr.try_into()?)) - } - } - - impl AsRef<[u8]> for $name { - fn as_ref(&self) -> &[u8] { - &self.0 - } - } - - impl Deref for $name { - type Target = [u8; $size]; - fn deref(&self) -> &Self::Target { - &self.0 - } - } - }; -} - -macro_rules! declare_byte_array_type_with_bech32 { - ($name:ident, $size:expr, $hrp:expr) => { - declare_byte_array_type!($name, $size); - impl Bech32Conversion for $name { - fn to_bech32(&self) -> Result { - self.0.to_vec().to_bech32_with_hrp($hrp) - } - fn from_bech32(s: &str) -> Result { - match Vec::::from_bech32_with_hrp(s, $hrp) { - Ok(v) => match Self::try_from(v) { - Ok(s) => Ok(s), - Err(_) => Err(Error::msg(format!( - "Bad vector input to {}", - stringify!($name) - ))), - }, - Err(e) => Err(e), - } - } - } - }; -} - -declare_byte_array_type!(BlockHash, 32); - -declare_byte_array_type!(TxHash, 32); - -declare_byte_array_type_with_bech32!(VRFKey, 32, "vrf_vk"); - /// Block info, shared across multiple messages #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BlockInfo { From 90a757ff460fb9aae6be3ee5da747e5268792ab1 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 12:45:54 +0100 Subject: [PATCH 41/44] Add FromHex impl for byte arrays --- common/src/byte_array.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/src/byte_array.rs b/common/src/byte_array.rs index b61c4e22..6ff5f786 100644 --- a/common/src/byte_array.rs +++ b/common/src/byte_array.rs @@ -1,5 +1,6 @@ use crate::serialization::{Bech32Conversion, Bech32WithHrp}; use anyhow::Error; +use hex::{FromHex, FromHexError}; use serde_with::{hex::Hex, serde_as}; use std::ops::Deref; @@ -18,6 +19,17 @@ macro_rules! declare_byte_array_type { } } + impl FromHex for $name { + type Error = FromHexError; + + fn from_hex>(hex: T) -> Result { + Ok(match Self::try_from(Vec::::from_hex(hex)?) { + Ok(b) => Ok(b), + Err(_) => Err(FromHexError::InvalidStringLength), + }?) + } + } + impl TryFrom> for $name { type Error = Vec; fn try_from(vec: Vec) -> Result { From acae2ee892011308b50065319645909a3f0197b9 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 12:47:58 +0100 Subject: [PATCH 42/44] Remove incorrect old comment --- modules/chain_store/src/chain_store.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 06086dde..d2cfd5e7 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -535,7 +535,6 @@ impl ChainStore { #[derive(Default, Debug, Clone)] pub struct State { - // Keyed on cert pub byron_heavy_delegates: HashMap, HeavyDelegate>, pub shelley_genesis_delegates: HashMap, GenesisDelegate>, } From 88dedb99006d8b8832364fcf1d81396237ffa4a4 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 13:11:35 +0100 Subject: [PATCH 43/44] Add self to authors --- modules/chain_store/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index 3ed9fc55..d4af79d6 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -2,7 +2,7 @@ name = "acropolis_module_chain_store" version = "0.1.0" edition = "2021" -authors = ["Simon Gellis "] +authors = ["Simon Gellis ","Alex Woods "] description = "Chain Store Tracker" license = "Apache-2.0" From 0e54ebd151d4c6193266ffb92ee7ea2e4959ba37 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 22 Oct 2025 13:13:04 +0100 Subject: [PATCH 44/44] cargo fmt run --- common/src/address.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/address.rs b/common/src/address.rs index f121cc20..d418366e 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -9,7 +9,6 @@ use minicbor::data::IanaTag; use serde_with::{hex::Hex, serde_as}; use std::cmp::Ordering; - /// a Byron-era address #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct ByronAddress {