borderless_runtime/db/
controller.rs

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