borderless_runtime/db/
controller.rs

1use borderless::{
2    common::{Description, Metadata, Revocation},
3    contracts::Info,
4    BorderlessId, ContractId,
5    __private::storage_keys::*,
6    events::Sink,
7    hash::Hash256,
8    http::{AgentInfo, ContractInfo},
9    pkg::{Source, SourceFlattened, WasmPkg, WasmPkgNoSource},
10    prelude::Id,
11    AgentId, TxIdentifier,
12};
13use borderless_kv_store::*;
14use serde::de::DeserializeOwned;
15
16use super::{
17    action_log::{ActionLog, ActionRecord, RelTxAction},
18    logger::Logger,
19};
20use crate::{Result, ACTION_TX_REL_SUB_DB, AGENT_SUB_DB, CONTRACT_SUB_DB};
21
22/// Model-controller to retrieve information about a contract from the key-value storage.
23pub struct Controller<'a, S: Db> {
24    db: &'a S,
25}
26
27impl<'a, S: Db> Controller<'a, S> {
28    pub fn new(db: &'a S) -> Self {
29        Self { db }
30    }
31
32    /// Returns the [`ActionLog`] of the contract
33    pub fn actions(&self, cid: ContractId) -> ActionLog<'a, S> {
34        ActionLog::new(self.db, cid)
35    }
36
37    /// Returns the [`Logger`] of the contract or agent
38    pub fn logs(&self, id: impl Into<Id>) -> Logger<'a, S> {
39        Logger::new(self.db, id)
40    }
41
42    /// List of contract-participants
43    pub fn contract_participants(&self, cid: &ContractId) -> Result<Option<Vec<BorderlessId>>> {
44        self.read_value(
45            &Id::contract(*cid),
46            BASE_KEY_METADATA,
47            META_SUB_KEY_PARTICIPANTS,
48        )
49    }
50
51    /// Returns `true` if the contract exists
52    pub fn contract_exists(&self, cid: &ContractId) -> Result<bool> {
53        Ok(self
54            .read_value::<ContractId>(
55                &Id::contract(*cid),
56                BASE_KEY_METADATA,
57                META_SUB_KEY_CONTRACT_ID,
58            )?
59            .is_some())
60    }
61
62    /// Returns `true` if the contract exists
63    pub fn agent_exists(&self, aid: &AgentId) -> Result<bool> {
64        Ok(self
65            .read_value::<AgentId>(
66                &Id::agent(*aid),
67                BASE_KEY_METADATA,
68                META_SUB_KEY_CONTRACT_ID,
69            )?
70            .is_some())
71    }
72
73    /// Returns `true` if the contract has been revoked
74    pub fn contract_revoked(&self, cid: &ContractId) -> Result<bool> {
75        Ok(self.contract_revoked_ts(cid)?.is_some())
76    }
77
78    /// Returns `true` if the agent has been revoked
79    pub fn agent_revoked(&self, aid: &AgentId) -> Result<bool> {
80        Ok(self.agent_revoked_ts(aid)?.is_some())
81    }
82
83    /// Returns the timestamp, when the contract has been revoked.
84    pub fn contract_revoked_ts(&self, cid: &ContractId) -> Result<Option<u64>> {
85        self.read_value::<u64>(
86            &Id::contract(*cid),
87            BASE_KEY_METADATA,
88            META_SUB_KEY_REVOKED_TS,
89        )
90    }
91
92    /// Returns the timestamp, when the agent has been revoked.
93    pub fn agent_revoked_ts(&self, aid: &AgentId) -> Result<Option<u64>> {
94        self.read_value::<u64>(&Id::agent(*aid), BASE_KEY_METADATA, META_SUB_KEY_REVOKED_TS)
95    }
96
97    /// Returns the hash of the last-tx that was executed by the contract
98    pub fn contract_last_tx_hash(&self, cid: &ContractId) -> Result<Option<Hash256>> {
99        let actions = ActionLog::new(self.db, *cid);
100        if let Some(action) = actions.last()? {
101            return Ok(Some(action.tx_ctx.tx_id.hash));
102        }
103        // If there are no actions, the contract could simply be just introduced.
104        // In this case, read the introduction and parse the tx_introduced value
105        match self.contract_meta(cid)? {
106            Some(meta) => Ok(meta.tx_ctx_introduction.map(|t| t.tx_id.hash)),
107            None => Ok(None),
108        }
109    }
110
111    /// Returns the [`Info`] section of the contract
112    pub fn contract_info(&self, cid: &ContractId) -> Result<Option<Info>> {
113        let id = Id::contract(*cid);
114        let participants = self.read_value(&id, BASE_KEY_METADATA, META_SUB_KEY_PARTICIPANTS)?;
115        let roles = self.read_value(&id, BASE_KEY_METADATA, META_SUB_KEY_ROLES)?;
116        let sinks = self.read_value(&id, BASE_KEY_METADATA, META_SUB_KEY_SINKS)?;
117        match (participants, roles, sinks) {
118            (Some(participants), Some(roles), Some(sinks)) => Ok(Some(Info {
119                contract_id: *cid,
120                participants,
121                roles,
122                sinks,
123            })),
124            _ => Ok(None),
125        }
126    }
127
128    /// Returns the sinks for an agent
129    ///
130    /// Since an agent has no participants of roles, the sinks and the agent-id are the only two things
131    /// that are left from the [`Info`] struct.
132    pub fn agent_sinks(&self, aid: &AgentId) -> Result<Option<Vec<Sink>>> {
133        let aid = Id::agent(*aid);
134        self.read_value(&aid, BASE_KEY_METADATA, META_SUB_KEY_SINKS)
135    }
136
137    /// Returns the [`Description`] of the contract
138    pub fn contract_desc(&self, cid: &ContractId) -> Result<Option<Description>> {
139        self.read_value(&Id::contract(*cid), BASE_KEY_METADATA, META_SUB_KEY_DESC)
140    }
141
142    /// Returns the [`Description`] of the agent
143    pub fn agent_desc(&self, aid: &AgentId) -> Result<Option<Description>> {
144        self.read_value(&Id::agent(*aid), BASE_KEY_METADATA, META_SUB_KEY_DESC)
145    }
146
147    /// Returns the [`Metadata`] of the contract
148    pub fn contract_meta(&self, cid: &ContractId) -> Result<Option<Metadata>> {
149        self.read_value(&Id::contract(*cid), BASE_KEY_METADATA, META_SUB_KEY_META)
150    }
151
152    /// Returns the [`Metadata`] of the agent
153    pub fn agent_meta(&self, aid: &AgentId) -> Result<Option<Metadata>> {
154        self.read_value(&Id::agent(*aid), BASE_KEY_METADATA, META_SUB_KEY_META)
155    }
156
157    /// Returns the full [`ContractInfo`], which bundles info, description and metadata.
158    pub fn contract_full(&self, cid: &ContractId) -> Result<Option<ContractInfo>> {
159        let info = self.contract_info(cid)?;
160        let desc = self.contract_desc(cid)?;
161        let meta = self.contract_meta(cid)?;
162        Ok(Some(ContractInfo { info, desc, meta }))
163    }
164
165    /// Returns the full [`ContractInfo`], which bundles info, description and metadata.
166    pub fn agent_full(&self, aid: &AgentId) -> Result<Option<AgentInfo>> {
167        let sinks = self.agent_sinks(aid)?.unwrap_or_default();
168        let desc = self.agent_desc(aid)?;
169        let meta = self.agent_meta(aid)?;
170        Ok(Some(AgentInfo {
171            agent_id: *aid,
172            sinks,
173            desc,
174            meta,
175        }))
176    }
177
178    /// Returns the [`Revocation`] of the contract, if any.
179    pub fn contract_revocation(&self, cid: &ContractId) -> Result<Option<Revocation>> {
180        self.read_value(
181            &Id::contract(*cid),
182            BASE_KEY_METADATA,
183            META_SUB_KEY_REVOCATION,
184        )
185    }
186
187    /// Returns the [`Revocation`] of the contract, if any.
188    pub fn agent_revocation(&self, aid: &AgentId) -> Result<Option<Revocation>> {
189        self.read_value(&Id::agent(*aid), BASE_KEY_METADATA, META_SUB_KEY_REVOCATION)
190    }
191
192    /// Queries an [`ActionRecord`] based on the [`TxIdentifier`]
193    pub fn query_action(&self, tx_id: &TxIdentifier) -> Result<Option<ActionRecord>> {
194        let tx_id_bytes = tx_id.to_bytes();
195        let relation = {
196            let rel_db = self.db.create_sub_db(ACTION_TX_REL_SUB_DB)?;
197            let txn = self.db.begin_ro_txn()?;
198            match txn.read(&rel_db, &tx_id_bytes)? {
199                Some(bytes) => RelTxAction::from_bytes(&bytes),
200                None => return Ok(None),
201            }
202        };
203        // Do a sanity-check before we return the record
204        match self
205            .actions(relation.cid)
206            .get(relation.action_idx as usize)?
207        {
208            Some(record) => {
209                debug_assert!(record.tx_ctx.tx_id == *tx_id, "tx-id must match");
210                Ok(Some(record))
211            }
212            None => Ok(None),
213        }
214    }
215
216    /// Returns the package definition for an agent
217    pub fn agent_pkg_def(&self, aid: &AgentId) -> Result<Option<WasmPkgNoSource>> {
218        self.read_value(
219            &Id::agent(*aid),
220            BASE_KEY_METADATA,
221            META_SUB_KEY_PACKAGE_DEF,
222        )
223    }
224
225    /// Returns the package definition for an agent
226    pub fn agent_pkg_source(&self, aid: &AgentId) -> Result<Option<Source>> {
227        let source: Option<SourceFlattened> = self.read_value(
228            &Id::agent(*aid),
229            BASE_KEY_METADATA,
230            META_SUB_KEY_PACKAGE_SOURCE,
231        )?;
232        Ok(source.map(|s| s.unflatten()))
233    }
234
235    /// Returns the package definition for an agent
236    pub fn agent_pkg_full(&self, aid: &AgentId) -> Result<Option<WasmPkg>> {
237        let pkg_def = self.agent_pkg_def(aid)?;
238        let source = self.agent_pkg_source(aid)?;
239        match (pkg_def, source) {
240            (Some(pkg), Some(source)) => Ok(Some(WasmPkg::from_def_and_source(pkg, source))),
241            _ => Ok(None),
242        }
243    }
244
245    /// Returns the package definition for an contract
246    pub fn contract_pkg_def(&self, aid: &ContractId) -> Result<Option<WasmPkgNoSource>> {
247        self.read_value(
248            &Id::contract(*aid),
249            BASE_KEY_METADATA,
250            META_SUB_KEY_PACKAGE_DEF,
251        )
252    }
253
254    /// Returns the package definition for an contract
255    pub fn contract_pkg_source(&self, aid: &ContractId) -> Result<Option<Source>> {
256        // NOTE: We write a flattened source to the disk, because postcard does not support all serde features
257        let source: Option<SourceFlattened> = self.read_value(
258            &Id::contract(*aid),
259            BASE_KEY_METADATA,
260            META_SUB_KEY_PACKAGE_SOURCE,
261        )?;
262        Ok(source.map(|s| s.unflatten()))
263    }
264
265    /// Returns the package definition for an contract
266    pub fn contract_pkg_full(&self, aid: &ContractId) -> Result<Option<WasmPkg>> {
267        let pkg_def = self.contract_pkg_def(aid)?;
268        let source = self.contract_pkg_source(aid)?;
269        match (pkg_def, source) {
270            (Some(pkg), Some(source)) => Ok(Some(WasmPkg::from_def_and_source(pkg, source))),
271            _ => Ok(None),
272        }
273    }
274
275    fn read_value<D: DeserializeOwned>(
276        &self,
277        id: &Id,
278        base_key: u64,
279        sub_key: u64,
280    ) -> Result<Option<D>> {
281        // Use correct sub-db based on the id-type
282        let db_ptr = match id {
283            Id::Contract { .. } => self.db.open_sub_db(CONTRACT_SUB_DB)?,
284            Id::Agent { .. } => self.db.open_sub_db(AGENT_SUB_DB)?,
285        };
286        let txn = self.db.begin_ro_txn()?;
287        let key = StorageKey::system_key(id, base_key, sub_key);
288        let bytes = txn.read(&db_ptr, &key)?;
289        let result = match bytes {
290            Some(val) => Some(postcard::from_bytes(val)?),
291            None => None,
292        };
293        txn.commit()?;
294        Ok(result)
295    }
296}
297
298// Helper function to write fields with system-keys
299#[cfg(any(feature = "contracts", feature = "agents"))]
300pub(crate) fn write_system_value<S: Db, D: serde::Serialize, ID: AsRef<[u8; 16]>>(
301    db_ptr: &S::Handle,
302    txn: &mut <S as Db>::RwTx<'_>,
303    uid: ID,
304    base_key: u64,
305    sub_key: u64,
306    data: &D,
307) -> Result<()> {
308    let key = StorageKey::system_key(uid, base_key, sub_key);
309    let bytes = postcard::to_allocvec(data)?;
310    txn.write(db_ptr, &key, &bytes)?;
311    Ok(())
312}
313
314// Helper function to write fields with system-keys
315#[cfg(any(feature = "contracts", feature = "agents"))]
316pub(crate) fn read_system_value<S: Db, D: DeserializeOwned, ID: AsRef<[u8; 16]>>(
317    db_ptr: &S::Handle,
318    txn: &<S as Db>::RwTx<'_>,
319    cid: ID,
320    base_key: u64,
321    sub_key: u64,
322) -> Result<Option<D>> {
323    let key = StorageKey::system_key(cid, base_key, sub_key);
324    let bytes = txn.read(db_ptr, &key)?;
325    match bytes {
326        Some(val) => {
327            let out = postcard::from_bytes(val)?;
328            Ok(Some(out))
329        }
330        None => Ok(None),
331    }
332}
333
334#[cfg(any(feature = "contracts", feature = "agents"))]
335pub(crate) fn write_introduction<S: Db>(
336    db_ptr: &S::Handle,
337    txn: &mut <S as Db>::RwTx<'_>,
338    introduction: borderless::common::Introduction,
339) -> Result<()> {
340    use borderless::__private::storage_keys::*;
341
342    use crate::error::ErrorKind;
343    let cid = introduction.id;
344
345    // NOTE: If the id was already written to disk, this means that the contract has already been written !
346    let check_id = read_system_value::<S, Id, _>(
347        db_ptr,
348        txn,
349        &cid,
350        BASE_KEY_METADATA,
351        META_SUB_KEY_CONTRACT_ID,
352    )?;
353    if check_id.is_some() {
354        return Err(ErrorKind::DoubleIntroduction.into());
355    }
356
357    // Write contract-id
358    write_system_value::<S, _, _>(
359        db_ptr,
360        txn,
361        &cid,
362        BASE_KEY_METADATA,
363        META_SUB_KEY_CONTRACT_ID,
364        &introduction.id,
365    )?;
366
367    // Write participant list
368    write_system_value::<S, _, _>(
369        db_ptr,
370        txn,
371        &cid,
372        BASE_KEY_METADATA,
373        META_SUB_KEY_PARTICIPANTS,
374        &introduction.participants,
375    )?;
376
377    // Write roles list
378    write_system_value::<S, _, _>(
379        db_ptr,
380        txn,
381        &cid,
382        BASE_KEY_METADATA,
383        META_SUB_KEY_ROLES,
384        &introduction.roles,
385    )?;
386
387    // Write sink list
388    write_system_value::<S, _, _>(
389        db_ptr,
390        txn,
391        &cid,
392        BASE_KEY_METADATA,
393        META_SUB_KEY_SINKS,
394        &introduction.sinks,
395    )?;
396
397    // Write description
398    write_system_value::<S, _, _>(
399        db_ptr,
400        txn,
401        &cid,
402        BASE_KEY_METADATA,
403        META_SUB_KEY_DESC,
404        &introduction.desc,
405    )?;
406
407    // Write meta
408    write_system_value::<S, _, _>(
409        db_ptr,
410        txn,
411        &cid,
412        BASE_KEY_METADATA,
413        META_SUB_KEY_META,
414        &introduction.meta,
415    )?;
416
417    // Write initial state
418    write_system_value::<S, _, _>(
419        db_ptr,
420        txn,
421        &cid,
422        BASE_KEY_METADATA,
423        META_SUB_KEY_INIT_STATE,
424        &introduction.initial_state,
425    )?;
426
427    // Write package and source (flattened, because postcard does not support untagged enums)
428    let (pkg_def, pkg_source) = introduction.package.into_def_and_source();
429    let pkg_source = pkg_source.flatten();
430
431    // Write pkg-def
432    write_system_value::<S, _, _>(
433        db_ptr,
434        txn,
435        &cid,
436        BASE_KEY_METADATA,
437        META_SUB_KEY_PACKAGE_DEF,
438        &pkg_def,
439    )?;
440
441    // Write pkg-source
442    write_system_value::<S, _, _>(
443        db_ptr,
444        txn,
445        &cid,
446        BASE_KEY_METADATA,
447        META_SUB_KEY_PACKAGE_SOURCE,
448        &pkg_source,
449    )?;
450
451    Ok(())
452}
453
454#[cfg(any(feature = "contracts", feature = "agents"))]
455pub(crate) fn write_revocation<S: Db>(
456    db_ptr: &S::Handle,
457    txn: &mut <S as Db>::RwTx<'_>,
458    revocation: &Revocation,
459    tx_ctx: borderless::contracts::TxCtx,
460    timestamp: u64,
461) -> Result<()> {
462    let cid = revocation.id;
463    // Update metadata field
464    let meta: Option<Metadata> =
465        read_system_value::<S, _, _>(db_ptr, txn, &cid, BASE_KEY_METADATA, META_SUB_KEY_META)?;
466    let mut meta = meta.unwrap();
467
468    meta.inactive_since = timestamp;
469    meta.tx_ctx_revocation = Some(tx_ctx);
470
471    write_system_value::<S, _, _>(
472        db_ptr,
473        txn,
474        &cid,
475        BASE_KEY_METADATA,
476        META_SUB_KEY_META,
477        &meta,
478    )?;
479    write_system_value::<S, _, _>(
480        db_ptr,
481        txn,
482        &cid,
483        BASE_KEY_METADATA,
484        META_SUB_KEY_REVOKED_TS,
485        &timestamp,
486    )?;
487    write_system_value::<S, _, _>(
488        db_ptr,
489        txn,
490        &cid,
491        BASE_KEY_METADATA,
492        META_SUB_KEY_REVOCATION,
493        &revocation,
494    )?;
495    Ok(())
496}