Skip to main content

borderless_runtime/db/
action_log.rs

1use borderless::http::queries::Pagination;
2use borderless::http::{PaginatedElements, TxAction};
3use borderless::ContractId;
4use borderless_kv_store::{Db, RawRead, Tx};
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7
8use borderless::contracts::TxCtx;
9
10use borderless::__private::storage_keys::{StorageKey, BASE_KEY_ACTION_LOG};
11
12#[cfg(any(feature = "contracts", feature = "agents"))]
13use borderless::events::CallAction;
14
15#[allow(unused_imports)]
16use crate::log_shim::*;
17use crate::{Result, CONTRACT_SUB_DB};
18
19/// Sub-Key where the length of the action-log is stored
20pub const SUB_KEY_LOG_LEN: u64 = u64::MAX;
21
22// NOTE: This is the relationship that we want to save in the KV-Storage, when it comes to the actions
23// - Action:
24//   - Key: contract-id:ACTION_KEY:action-index
25//   - Value: Action + Tx-Identifier
26//   - Related to: Tx
27// - Rel Tx->Action: link tx-identifier with contract-id + action-index
28// - Tx:
29//   - Key: chain-id:block-number:block-tx-number
30//   - Value: Tx-Info + Block-Id
31//   - Related to: Block
32// - Block:
33//   - Key: chain-id:block-number
34//   - Value: Block-Header + List of Txs
35//   - Related to: Tx
36// - Relate Tx-Hash -> chain-id:block-number:block-tx-number
37
38/// The `ActionLog` records all actions that were successfully executed by the contract.
39///
40/// Since the action is fed into the contract as json-encoded bytes, we record exactly the raw json-bytes here,
41/// and not the `CallAction` object. This allows us to efficiently give out the json object,
42/// because instead of deserializing and then serializing it back to json, we can directly copy the json data after deserialization.
43pub struct ActionLog<'a, S: Db> {
44    db: &'a S,
45    cid: ContractId,
46}
47
48/// The `ActionRecord` is used to record actions in the [`ActionLog`].
49///
50/// The record bundles the raw json-bytes of the action together with meta-information like the transaction identifier
51/// and transaction sequence number (which is the index of the transaction inside the block).
52#[derive(Serialize, Deserialize)]
53pub struct ActionRecord {
54    pub tx_ctx: TxCtx,
55
56    /// Action value as raw bytes.
57    ///
58    /// Since all incoming events are encoded in json, we directly save the json bytes here.
59    /// This enables us to later directly spit out the json (for usage via api e.g.) without having to serialize it back.
60    ///
61    /// Note: This must decode to a [`CallAction`] object.
62    #[serde(with = "serde_bytes")]
63    pub value: Vec<u8>,
64
65    /// Timestamp (as milliseconds since unix-epoch), when the action was commited.
66    pub commited: u64,
67}
68
69impl TryFrom<ActionRecord> for TxAction {
70    type Error = serde_json::Error;
71
72    fn try_from(record: ActionRecord) -> std::result::Result<Self, Self::Error> {
73        // Hm, I thought we could get around the additional parsing step here..
74        // I still haven't given up ! TODO maybe construct the raw json value here, and see if this is faster.
75        let action = serde_json::from_slice(&record.value)?;
76        Ok(Self {
77            tx_id: record.tx_ctx.tx_id,
78            action,
79            commited: record.commited,
80        })
81    }
82}
83
84/// Relationship between an action and a transaction
85///
86/// This model is saved behind the key of a tx-identifier,
87/// to be able to relate a transaction back to the action.
88pub struct RelTxAction {
89    /// Contract-ID of the action's contract
90    pub cid: ContractId,
91    /// Index (number) of the action inside the contract
92    pub action_idx: u64,
93}
94
95impl RelTxAction {
96    /// Converts the `RelTxAction` into bytes
97    pub fn into_bytes(self) -> [u8; 24] {
98        let cid_bytes = self.cid.into_bytes();
99        let idx_bytes = self.action_idx.to_be_bytes();
100        let mut buf = [0u8; 24];
101        buf[..16].copy_from_slice(&cid_bytes);
102        buf[16..].copy_from_slice(&idx_bytes);
103        buf
104    }
105
106    /// Parses the relation from bytes
107    ///
108    /// # Panics
109    ///
110    /// This function panics, if the byte-slice is not an encoded [`RelTxAction`] object.
111    pub fn from_bytes(bytes: &[u8]) -> Self {
112        if bytes.len() != 24 {
113            panic!("invalid slice length - expected 24 bytes");
114        }
115        let mut cid_bytes = [0u8; 16];
116        cid_bytes.copy_from_slice(&bytes[..16]);
117        let cid = ContractId::from_bytes(cid_bytes);
118        let mut idx_bytes = [0u8; 8];
119        idx_bytes.copy_from_slice(&bytes[16..]);
120        let action_idx = u64::from_be_bytes(idx_bytes);
121        Self { cid, action_idx }
122    }
123}
124
125impl<'a, S: Db> ActionLog<'a, S> {
126    /// Opens (or creates) the action log
127    pub fn new(db: &'a S, cid: ContractId) -> Self {
128        Self { db, cid }
129    }
130
131    #[cfg(any(feature = "contracts", feature = "agents"))]
132    pub(crate) fn commit(
133        self,
134        db_ptr: &S::Handle,
135        txn: &mut <S as Db>::RwTx<'_>,
136        action: &CallAction,
137        tx_ctx: TxCtx,
138    ) -> Result<()> {
139        use borderless_kv_store::RawWrite;
140
141        use crate::ACTION_TX_REL_SUB_DB;
142
143        use super::controller::{read_system_value, write_system_value};
144        use std::time::{SystemTime, UNIX_EPOCH};
145
146        let timestamp = SystemTime::now()
147            .duration_since(UNIX_EPOCH)
148            .expect("timestamp < 1970")
149            .as_millis()
150            .try_into()
151            .expect("u64 should fit for 584942417 years");
152
153        let len_commited: u64 = {
154            read_system_value::<S, _, _>(
155                db_ptr,
156                txn,
157                &self.cid,
158                BASE_KEY_ACTION_LOG,
159                SUB_KEY_LOG_LEN,
160            )?
161            .unwrap_or_default()
162        };
163
164        let full_len = len_commited + 1;
165        let sub_key = len_commited;
166        let value = ActionRecord {
167            tx_ctx,
168            value: action.to_bytes()?,
169            commited: timestamp,
170        };
171        write_system_value::<S, _, _>(
172            db_ptr,
173            txn,
174            &self.cid,
175            BASE_KEY_ACTION_LOG,
176            sub_key,
177            &value,
178        )?;
179        write_system_value::<S, _, _>(
180            db_ptr,
181            txn,
182            &self.cid,
183            BASE_KEY_ACTION_LOG,
184            SUB_KEY_LOG_LEN,
185            &full_len,
186        )?;
187
188        // Store relationship - this is just another sub-db, and outside the "normal" contract keyspace
189        let rel_db = self.db.open_sub_db(ACTION_TX_REL_SUB_DB)?;
190        let tx_id_bytes = value.tx_ctx.tx_id.to_bytes();
191        let relationship = RelTxAction {
192            cid: self.cid,
193            action_idx: sub_key,
194        };
195        txn.write(&rel_db, &tx_id_bytes, &relationship.into_bytes())?;
196
197        debug!("Commited action to log. len={full_len}");
198        Ok(())
199    }
200
201    /// Retrieves a value from the log
202    pub fn get(&self, idx: usize) -> Result<Option<ActionRecord>> {
203        let idx = idx as u64;
204        let len_commited = self.len()?;
205        debug_assert!(idx < SUB_KEY_LOG_LEN);
206        if idx < len_commited {
207            self.read_value(BASE_KEY_ACTION_LOG, idx)
208        } else {
209            Ok(None)
210        }
211    }
212
213    // TODO: Add 'reverse' option
214    pub fn get_tx_action_paginated(
215        &self,
216        pagination: Pagination,
217    ) -> Result<Option<PaginatedElements<TxAction>>> {
218        // Get actions
219        let n_actions = self.len()?;
220
221        let mut elements = Vec::new();
222        for idx in pagination.to_range() {
223            // TODO: We can utilize the action log here !
224            match self.read_value::<ActionRecord>(BASE_KEY_ACTION_LOG, idx as u64)? {
225                Some(record) => {
226                    let action = TxAction::try_from(record)?;
227                    elements.push(action);
228                }
229                None => break,
230            }
231        }
232        let paginated = PaginatedElements {
233            elements,
234            total_elements: n_actions as usize,
235            pagination,
236        };
237        Ok(Some(paginated))
238    }
239
240    /// Retrieves the last action record
241    pub fn last(&self) -> Result<Option<ActionRecord>> {
242        let len_commited = self.len()?;
243        self.read_value(BASE_KEY_ACTION_LOG, len_commited.saturating_sub(1))
244    }
245
246    pub fn len(&self) -> Result<u64> {
247        Ok(self
248            .read_value(BASE_KEY_ACTION_LOG, SUB_KEY_LOG_LEN)?
249            .unwrap_or_default())
250    }
251
252    pub fn is_empty(&self) -> Result<bool> {
253        Ok(self.len()? == 0)
254    }
255
256    fn read_value<D: DeserializeOwned>(&self, base_key: u64, sub_key: u64) -> Result<Option<D>> {
257        let db_ptr = self.db.open_sub_db(CONTRACT_SUB_DB)?;
258        let txn = self.db.begin_ro_txn()?;
259        let key = StorageKey::system_key(self.cid, base_key, sub_key);
260        let bytes = txn.read(&db_ptr, &key)?;
261        let result = match bytes {
262            Some(val) => Some(postcard::from_bytes(val)?),
263            None => None,
264        };
265        txn.commit()?;
266        Ok(result)
267    }
268
269    /// Returns an iterator over all action-records
270    pub fn iter(&self) -> Iter<'_, S> {
271        Iter { log: self, idx: 0 }
272    }
273}
274
275/// Iterator over the [`ActionLog`]
276pub struct Iter<'a, S: Db> {
277    log: &'a ActionLog<'a, S>,
278    idx: usize,
279}
280
281impl<'a, S: Db> Iterator for Iter<'a, S> {
282    type Item = Result<ActionRecord>;
283
284    fn next(&mut self) -> Option<Self::Item> {
285        let idx = self.idx;
286        self.idx += 1;
287        self.log.get(idx).transpose()
288    }
289}