aztec_pxe/stores/
anchor_block_store.rs

1//! Anchor block store for persisting the current synced block header.
2//!
3//! Ports the TS `AnchorBlockStore` which stores the current synchronized
4//! block header. Updated only from the BlockStateSynchronizer.
5
6use std::sync::Arc;
7
8use aztec_core::error::Error;
9use serde::{Deserialize, Serialize};
10
11use super::kv::KvStore;
12
13/// Key used to store the anchor block header.
14const ANCHOR_HEADER_KEY: &[u8] = b"anchor:header";
15
16/// Stored anchor block header with extracted metadata.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AnchorBlockHeader {
19    /// The full block header data (opaque JSON matching TS types).
20    pub data: serde_json::Value,
21    /// Extracted block number for convenience.
22    pub block_number: u64,
23    /// Extracted block hash for convenience.
24    pub block_hash: String,
25}
26
27impl AnchorBlockHeader {
28    /// Extract block number from the header JSON.
29    pub fn get_block_number(&self) -> u64 {
30        self.block_number
31    }
32
33    /// Extract block hash from the header JSON.
34    pub fn get_block_hash(&self) -> &str {
35        &self.block_hash
36    }
37
38    /// Create from raw header JSON, extracting metadata.
39    pub fn from_header_json(data: serde_json::Value) -> Self {
40        let block_number = data
41            .pointer("/globalVariables/blockNumber")
42            .and_then(|v| {
43                v.as_u64().or_else(|| {
44                    v.as_str().and_then(|s| {
45                        if let Some(hex) = s.strip_prefix("0x") {
46                            u64::from_str_radix(hex, 16).ok()
47                        } else {
48                            s.parse::<u64>().ok()
49                        }
50                    })
51                })
52            })
53            .unwrap_or(0);
54
55        let block_hash = data
56            .pointer("/blockHash")
57            .or_else(|| data.get("blockHash"))
58            .and_then(|v| v.as_str())
59            .unwrap_or("0x0")
60            .to_owned();
61
62        Self {
63            data,
64            block_number,
65            block_hash,
66        }
67    }
68}
69
70/// Stores the current anchor (synced) block header.
71///
72/// The anchor block header determines which state the PXE considers current.
73/// It is updated by the BlockStateSynchronizer and read during transaction
74/// simulation and event retrieval.
75pub struct AnchorBlockStore {
76    kv: Arc<dyn KvStore>,
77}
78
79impl AnchorBlockStore {
80    pub fn new(kv: Arc<dyn KvStore>) -> Self {
81        Self { kv }
82    }
83
84    /// Set the anchor block header.
85    pub async fn set_header(&self, header: &AnchorBlockHeader) -> Result<(), Error> {
86        let value = serde_json::to_vec(header)?;
87        self.kv.put(ANCHOR_HEADER_KEY, &value).await
88    }
89
90    /// Get the anchor block header.
91    ///
92    /// Returns `None` if no header has been set yet.
93    pub async fn get_block_header(&self) -> Result<Option<AnchorBlockHeader>, Error> {
94        match self.kv.get(ANCHOR_HEADER_KEY).await? {
95            Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
96            None => Ok(None),
97        }
98    }
99
100    /// Get the anchor block header, returning an error if not set.
101    pub async fn get_block_header_or_err(&self) -> Result<AnchorBlockHeader, Error> {
102        self.get_block_header()
103            .await?
104            .ok_or_else(|| Error::InvalidData("anchor block header not set".into()))
105    }
106
107    /// Get the current anchor block number, or 0 if not set.
108    pub async fn get_block_number(&self) -> Result<u64, Error> {
109        match self.get_block_header().await? {
110            Some(header) => Ok(header.block_number),
111            None => Ok(0),
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::stores::InMemoryKvStore;
120
121    #[tokio::test]
122    async fn set_and_get_header() {
123        let kv = Arc::new(InMemoryKvStore::new());
124        let store = AnchorBlockStore::new(kv);
125
126        let header = AnchorBlockHeader::from_header_json(serde_json::json!({
127            "globalVariables": {"blockNumber": 42},
128            "blockHash": "0xabc123"
129        }));
130
131        store.set_header(&header).await.unwrap();
132        let retrieved = store.get_block_header().await.unwrap().unwrap();
133        assert_eq!(retrieved.block_number, 42);
134        assert_eq!(retrieved.block_hash, "0xabc123");
135    }
136
137    #[tokio::test]
138    async fn get_header_returns_none_when_unset() {
139        let kv = Arc::new(InMemoryKvStore::new());
140        let store = AnchorBlockStore::new(kv);
141        assert!(store.get_block_header().await.unwrap().is_none());
142    }
143
144    #[tokio::test]
145    async fn get_header_or_err_fails_when_unset() {
146        let kv = Arc::new(InMemoryKvStore::new());
147        let store = AnchorBlockStore::new(kv);
148        assert!(store.get_block_header_or_err().await.is_err());
149    }
150
151    #[tokio::test]
152    async fn from_header_json_extracts_block_number() {
153        let header = AnchorBlockHeader::from_header_json(serde_json::json!({
154            "globalVariables": {"blockNumber": 100}
155        }));
156        assert_eq!(header.block_number, 100);
157    }
158
159    #[tokio::test]
160    async fn from_header_json_handles_hex_block_number() {
161        let header = AnchorBlockHeader::from_header_json(serde_json::json!({
162            "globalVariables": {"blockNumber": "0x0a"}
163        }));
164        assert_eq!(header.block_number, 10);
165    }
166
167    #[tokio::test]
168    async fn set_header_overwrites_previous() {
169        let kv = Arc::new(InMemoryKvStore::new());
170        let store = AnchorBlockStore::new(kv);
171
172        let h1 = AnchorBlockHeader::from_header_json(serde_json::json!({
173            "globalVariables": {"blockNumber": 1}
174        }));
175        let h2 = AnchorBlockHeader::from_header_json(serde_json::json!({
176            "globalVariables": {"blockNumber": 2}
177        }));
178
179        store.set_header(&h1).await.unwrap();
180        store.set_header(&h2).await.unwrap();
181
182        let retrieved = store.get_block_header().await.unwrap().unwrap();
183        assert_eq!(retrieved.block_number, 2);
184    }
185}