Skip to main content

borderless_runtime/db/
ledger.rs

1//! Ledger related functionality
2//!
3//! A ledger between two parties is persisted in the kv-store and can be modified through the ledger-api.
4
5use std::array::TryFromSliceError;
6
7use ahash::{HashMap, HashMapExt};
8use borderless::{
9    contracts::ledger::{Currency, LedgerEntry, Money},
10    http::{queries::Pagination, PaginatedElements},
11    prelude::{ledger::EntryType, TxCtx},
12    BorderlessId, Context, ContractId,
13};
14use borderless_kv_store::{Db, RawRead, RawWrite, RoCursor as _, RoTx};
15use serde::{Deserialize, Serialize};
16
17use crate::{Error, Result, LEDGER_SUB_DB};
18
19use crate::log_shim::debug;
20
21/// Ledger controller of the database
22pub struct Ledger<'a, S: Db> {
23    db: &'a S,
24}
25
26impl<'a, S: Db> Ledger<'a, S> {
27    pub fn new(db: &'a S) -> Self {
28        Self { db }
29    }
30
31    /// Commits a new ledger-entry in the given transaction
32    pub(crate) fn commit_entry(
33        &self,
34        txn: &mut <S as Db>::RwTx<'_>,
35        entry: &LedgerEntry,
36        cid: ContractId,
37        tx_ctx: &TxCtx,
38    ) -> Result<()> {
39        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
40        // Read current ledger meta information
41        let ledger_id = entry.creditor.merge_compact(&entry.debitor);
42        let meta_key = LedgerKey::meta(ledger_id);
43        let mut meta = match txn.read(&db_ptr, &meta_key)? {
44            Some(val) => postcard::from_bytes(val)?,
45            None => LedgerMeta::new(entry.creditor, entry.debitor),
46        };
47
48        // Write ledger line
49        let c_key = LedgerKey::new(ledger_id, meta.len, "creditor");
50        let d_key = LedgerKey::new(ledger_id, meta.len, "debitor");
51        let amount_key = LedgerKey::new(ledger_id, meta.len, "amount");
52        let tax_key = LedgerKey::new(ledger_id, meta.len, "tax");
53        let currency_key = LedgerKey::new(ledger_id, meta.len, "currency");
54        let kind_key = LedgerKey::new(ledger_id, meta.len, "kind");
55        let tag_key = LedgerKey::new(ledger_id, meta.len, "tag");
56        let cid_key = LedgerKey::new(ledger_id, meta.len, "contract_id");
57        let tx_ctx_key = LedgerKey::new(ledger_id, meta.len, "tx_ctx");
58        let tx_ctx_bytes = postcard::to_allocvec(&tx_ctx)?;
59        txn.write(&db_ptr, &c_key, entry.creditor.as_bytes())?;
60        txn.write(&db_ptr, &d_key, entry.debitor.as_bytes())?;
61        txn.write(&db_ptr, &amount_key, &entry.amount_milli.to_be_bytes())?;
62        txn.write(&db_ptr, &tax_key, &entry.tax_milli.to_be_bytes())?;
63        txn.write(&db_ptr, &currency_key, &entry.currency.to_be_bytes())?;
64        txn.write(&db_ptr, &kind_key, &entry.kind.to_be_bytes())?;
65        txn.write(&db_ptr, &tag_key, &entry.tag.as_bytes())?;
66        txn.write(&db_ptr, &cid_key, &cid.as_bytes())?;
67        txn.write(&db_ptr, &tx_ctx_key, &tx_ctx_bytes)?;
68
69        // update meta information based on the current entry
70        meta.update(entry)?;
71
72        // Write meta back
73        let meta_bytes = postcard::to_allocvec(&meta)?;
74        txn.write(&db_ptr, &meta_key, &meta_bytes)?;
75        debug!(
76            "commited ledger entry: {entry}, ledger-id={ledger_id}, len={}",
77            meta.len
78        );
79        Ok(())
80    }
81
82    /// Opens a ledger for a pair of borderless-ids
83    pub fn open(&self, p1: BorderlessId, p2: BorderlessId) -> SelectedLedger<'a, S> {
84        let ledger_id = p1.merge_compact(&p2);
85        SelectedLedger {
86            db: self.db,
87            ledger_id,
88        }
89    }
90
91    /// Selects a specific ledger based on its ID
92    pub fn select(&self, ledger_id: u64) -> SelectedLedger<'a, S> {
93        SelectedLedger {
94            db: self.db,
95            ledger_id,
96        }
97    }
98
99    /// Returns a list of all existing ledgers
100    pub fn all(&self) -> Result<Vec<LedgerMeta>> {
101        let mut out = Vec::new();
102        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
103        let txn = self.db.begin_ro_txn()?;
104        let mut cursor = txn.ro_cursor(&db_ptr)?;
105
106        // NOTE: We always iterate over the entire key-space.
107        // As this is all super low level, it is quite efficient,
108        // but on paper it does not scale very well.
109        // In the far or near future we have to migrate this to something different.
110        // (right now - parsing through 3TB of ledger data takes 300ms, so we have some time until this becomes relevant)
111        for (key, value) in cursor.iter() {
112            let key = LedgerKey::from_slice(key);
113            if key.is_meta() {
114                let ledger_meta = postcard::from_bytes(value)?;
115                out.push(ledger_meta);
116            }
117        }
118        Ok(out)
119    }
120
121    /// Returns a list of all existing ledgers
122    pub fn all_paginated(
123        &self,
124        pagination: Pagination,
125    ) -> Result<PaginatedElements<LedgerMetaDto>> {
126        let mut elements = Vec::new();
127        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
128        let txn = self.db.begin_ro_txn()?;
129        let mut cursor = txn.ro_cursor(&db_ptr)?;
130
131        let range = pagination.to_range();
132        let mut idx = 0;
133
134        // NOTE: We always iterate over the entire key-space.
135        // As this is all super low level, it is quite efficient,
136        // but on paper it does not scale very well.
137        // In the far or near future we have to migrate this to something different.
138        // (right now - parsing through 3TB of ledger data takes 300ms, so we have some time until this becomes relevant)
139        for (key, value) in cursor.iter() {
140            let key = LedgerKey::from_slice(key);
141            if !key.is_meta() {
142                continue;
143            }
144            if range.start <= idx && idx < range.end {
145                let ledger_meta: LedgerMeta = postcard::from_bytes(value)?;
146                elements.push(ledger_meta.into_dto());
147            }
148            idx += 1;
149        }
150        let paginated = PaginatedElements {
151            elements,
152            total_elements: idx,
153            pagination,
154        };
155        Ok(paginated)
156    }
157
158    pub fn all_ids(&self) -> Result<Vec<LedgerIds>> {
159        let mut out = Vec::new();
160        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
161        let txn = self.db.begin_ro_txn()?;
162        let mut cursor = txn.ro_cursor(&db_ptr)?;
163        let mut last_ledger_id = 0;
164        for (key, _) in cursor.iter() {
165            let key = LedgerKey::from_slice(key);
166            let ledger_id = key.ledger_id();
167            if ledger_id == last_ledger_id {
168                continue;
169            }
170            last_ledger_id = ledger_id;
171            // Read creditor and debitor
172            let elem = self.get_ledger_id(&txn, &db_ptr, ledger_id, key.line())?;
173            out.push(elem);
174        }
175        Ok(out)
176    }
177
178    pub fn all_ids_paginated(
179        &self,
180        pagination: Pagination,
181    ) -> Result<PaginatedElements<LedgerIds>> {
182        let mut elements = Vec::new();
183        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
184        let txn = self.db.begin_ro_txn()?;
185        let mut cursor = txn.ro_cursor(&db_ptr)?;
186        let mut last_ledger_id = 0;
187
188        let range = pagination.to_range();
189        let mut idx = 0;
190
191        // NOTE: We always iterate over the entire key-space.
192        // As this is all super low level, it is quite efficient,
193        // but on paper it does not scale very well.
194        // In the far or near future we have to migrate this to something different.
195        // (right now - parsing through 3TB of ledger data takes 300ms, so we have some time until this becomes relevant)
196        for (key, _) in cursor.iter() {
197            let key = LedgerKey::from_slice(key);
198            let ledger_id = key.ledger_id();
199            if ledger_id == last_ledger_id {
200                continue;
201            }
202            last_ledger_id = ledger_id;
203
204            if range.start <= idx && idx < range.end {
205                // Read creditor and debitor
206                let elem = self.get_ledger_id(&txn, &db_ptr, ledger_id, key.line())?;
207                elements.push(elem);
208            }
209            idx += 1;
210        }
211        let paginated = PaginatedElements {
212            elements,
213            total_elements: idx,
214            pagination,
215        };
216        Ok(paginated)
217    }
218
219    /// Helper function to obtain the `LedgerIds` from a single line.
220    /// If the line does not exists, it returns an error.
221    fn get_ledger_id(
222        &self,
223        txn: &<S as Db>::RoTx<'_>,
224        db_ptr: &<S as Db>::Handle,
225        ledger_id: u64,
226        line: u64,
227    ) -> Result<LedgerIds> {
228        // Read creditor and debitor
229        let c_key = LedgerKey::new(ledger_id, line, "creditor");
230        let d_key = LedgerKey::new(ledger_id, line, "debitor");
231        let creditor = txn
232            .read(db_ptr, &c_key)?
233            .and_then(|b| BorderlessId::from_slice(b).ok())
234            .context("missing creditor")?;
235        let debitor = txn
236            .read(db_ptr, &d_key)?
237            .and_then(|b| BorderlessId::from_slice(b).ok())
238            .context("missing debitor")?;
239        Ok(LedgerIds {
240            creditor,
241            debitor,
242            ledger_id,
243        })
244    }
245}
246
247/// Represents a selected ledger
248pub struct SelectedLedger<'a, S: Db> {
249    db: &'a S,
250    ledger_id: u64,
251}
252
253impl<'a, S: Db> SelectedLedger<'a, S> {
254    /// Returns the length of the ledger (if it exists)
255    pub fn meta(&self) -> Result<Option<LedgerMeta>> {
256        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
257        let key = LedgerKey::meta(self.ledger_id);
258        let txn = self.db.begin_ro_txn()?;
259        match txn.read(&db_ptr, &key)? {
260            Some(val) => {
261                let out = postcard::from_bytes(val)?;
262                Ok(Some(out))
263            }
264            None => Ok(None),
265        }
266    }
267
268    /// Returns the length of the ledger (if it exists)
269    pub fn meta_for_contract(&self, cid: ContractId) -> Result<Option<LedgerMetaDto>> {
270        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
271        let txn = self.db.begin_ro_txn()?;
272
273        // Read ledger meta
274        let key = LedgerKey::meta(self.ledger_id);
275        let mut meta: LedgerMeta = match txn
276            .read(&db_ptr, &key)?
277            .and_then(|b| postcard::from_bytes(b).ok())
278        {
279            Some(m) => m,
280            None => return Ok(None),
281        };
282        // Reset it, to only keep the creditor -> debitor info
283        meta.reset_balance();
284
285        let mut line = 0;
286        loop {
287            // If there are no more lines to read, then break
288            match self.check_line(&txn, &db_ptr, line as u64, cid)? {
289                Some(true) => { /* execute the logic below */ }
290                Some(false) => {
291                    line += 1;
292                    continue;
293                }
294                None => break,
295            }
296            let (entry, entry_cid, _tx_ctx) = self
297                .get(&txn, &db_ptr, line as u64)?
298                .context("line must exist")?;
299            debug_assert_eq!(entry_cid, cid);
300            // Update the ledger-meta based on the new entry
301            meta.update(&entry)?;
302            line += 1;
303        }
304        let mut dto = meta.into_dto();
305        dto.contract_id = Some(cid);
306        Ok(Some(dto))
307    }
308
309    /// Returns a paginated list of ledger entries (for all contracts)
310    pub fn get_entries_paginated(
311        &self,
312        pagination: Pagination,
313    ) -> Result<PaginatedElements<LedgerEntryDto>> {
314        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
315        let txn = self.db.begin_ro_txn()?;
316
317        // Read length via meta
318        let meta_key = LedgerKey::meta(self.ledger_id);
319        let total_elements = match txn
320            .read(&db_ptr, &meta_key)?
321            .and_then(|b| postcard::from_bytes::<LedgerMeta>(b).ok())
322        {
323            Some(meta) => meta.len as usize,
324            None => return Ok(PaginatedElements::empty(pagination)),
325        };
326
327        let mut elements = Vec::new();
328        if !pagination.reverse {
329            for idx in pagination.to_range() {
330                match self.get(&txn, &db_ptr, idx as u64)? {
331                    Some((entry, cid, tx_ctx)) => {
332                        elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
333                    }
334                    None => break,
335                }
336            }
337        } else {
338            let range = pagination.to_range();
339            let mut idx = total_elements.saturating_sub(range.start);
340            while idx > 0 {
341                // NOTE: We start with idx == total_elements if range.start == 0;
342                // So we decrease in advance. Otherwise the idx > 0 would result in us leaving out the last element.
343                idx -= 1;
344                let (entry, cid, tx_ctx) = self
345                    .get(&txn, &db_ptr, idx as u64)?
346                    .context("entry idx < max_len must exist")?;
347                elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
348                if range.end - range.start <= elements.len() {
349                    break;
350                }
351            }
352        }
353        Ok(PaginatedElements {
354            elements,
355            total_elements,
356            pagination,
357        })
358    }
359
360    pub fn get_contract_paginated(
361        &self,
362        cid: ContractId,
363        pagination: Pagination,
364    ) -> Result<PaginatedElements<LedgerEntryDto>> {
365        let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
366        let txn = self.db.begin_ro_txn()?;
367
368        let mut elements = Vec::new();
369        let range = pagination.to_range();
370        let mut idx = 0;
371
372        if !pagination.reverse {
373            // Go forward and ignore all entries, where the contract-id does not match
374            let mut line = 0;
375            loop {
376                // If there are no more lines to read, then break
377                match self.check_line(&txn, &db_ptr, line as u64, cid)? {
378                    Some(true) => { /* execute the logic below */ }
379                    Some(false) => {
380                        line += 1;
381                        continue;
382                    }
383                    None => break,
384                }
385                // Take as many items as fit in the page
386                if range.start <= idx && idx < range.end {
387                    let (entry, cid, tx_ctx) = self
388                        .get(&txn, &db_ptr, line as u64)?
389                        .context("line must exist")?;
390                    elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
391                }
392                // Keep counting, since we don't know the 'total_elements' in advance
393                idx += 1;
394                line += 1;
395            }
396        } else {
397            // Go backward and ignore all entries, where the contract-id does not match
398            let meta_key = LedgerKey::meta(self.ledger_id);
399            let all_entries = match txn
400                .read(&db_ptr, &meta_key)?
401                .and_then(|b| postcard::from_bytes::<LedgerMeta>(b).ok())
402            {
403                Some(meta) => meta.len as usize,
404                None => return Ok(PaginatedElements::empty(pagination)),
405            };
406
407            let mut line = all_entries;
408            while line > 0 {
409                // NOTE: We start with line == total_elements
410                // So we decrease in advance. Otherwise the line > 0 would result in us leaving out the last element.
411                line -= 1;
412                // if it returns 'false' we want to continue, as this is a line not matching to our contract-id
413                if !self
414                    .check_line(&txn, &db_ptr, line as u64, cid)?
415                    .unwrap_or_default()
416                {
417                    continue;
418                }
419                if range.start <= idx && idx < range.end {
420                    let (entry, cid, tx_ctx) = self
421                        .get(&txn, &db_ptr, line as u64)?
422                        .context("line must exist")?;
423                    elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
424                }
425                idx += 1;
426            }
427        }
428        Ok(PaginatedElements {
429            elements,
430            total_elements: idx,
431            pagination,
432        })
433    }
434
435    /// Reads a single column in an existing db-txn
436    fn read_column<T>(
437        &self,
438        txn: &<S as Db>::RoTx<'_>,
439        db_ptr: &<S as Db>::Handle,
440        line: u64,
441        column: &'static str,
442        transformer: impl Fn(&[u8]) -> Option<T>,
443    ) -> Result<Option<T>> {
444        let key = LedgerKey::new(self.ledger_id, line, column);
445        let out = txn.read(db_ptr, &key)?.and_then(transformer);
446        Ok(out)
447    }
448
449    /// Checks if a ledger entry exists and matches a specific contract-id
450    ///
451    /// Returns `Some(true)` if the line matches the contract-id and `Some(false)` if not.
452    /// If no line is found for the given index, `None` is returned.
453    fn check_line(
454        &self,
455        txn: &<S as Db>::RoTx<'_>,
456        db_ptr: &<S as Db>::Handle,
457        line: u64,
458        target_cid: ContractId,
459    ) -> Result<Option<bool>> {
460        let contract_id = match self.read_column(txn, db_ptr, line, "contract_id", |b| {
461            ContractId::from_slice(b).ok()
462        })? {
463            Some(c) => c,
464            None => {
465                // Early return here - if this value exists, all the others must exist too
466                return Ok(None);
467            }
468        };
469        // If we only want to match a single contract-id, we can do it like this:
470        Ok(Some(target_cid == contract_id))
471    }
472
473    /// Reads a line from the ledger
474    fn get(
475        &self,
476        txn: &<S as Db>::RoTx<'_>,
477        db_ptr: &<S as Db>::Handle,
478        line: u64,
479    ) -> Result<Option<(LedgerEntry, ContractId, TxCtx)>> {
480        let contract_id = match self.read_column(txn, db_ptr, line, "contract_id", |b| {
481            ContractId::from_slice(b).ok()
482        })? {
483            Some(c) => c,
484            None => {
485                // Early return here - if this value exists, all the others must exist too
486                return Ok(None);
487            }
488        };
489        // Read all other values
490        let creditor = self
491            .read_column(txn, db_ptr, line, "creditor", |b| {
492                BorderlessId::from_slice(b).ok()
493            })?
494            .context("missing creditor")?;
495        let debitor = self
496            .read_column(txn, db_ptr, line, "debitor", |b| {
497                BorderlessId::from_slice(b).ok()
498            })?
499            .context("missing debitor")?;
500        let amount_milli = self
501            .read_column(txn, db_ptr, line, "amount", i64_from_slice)?
502            .context("missing field amount")?;
503        let tax_milli = self
504            .read_column(txn, db_ptr, line, "tax", i64_from_slice)?
505            .context("missing field tax")?;
506        let currency = self
507            .read_column(txn, db_ptr, line, "currency", Currency::from_be_bytes)?
508            .context("missing field currency")?;
509        let kind = self
510            .read_column(txn, db_ptr, line, "kind", EntryType::from_be_bytes)?
511            .context("missing field tag")?;
512        let tag = self
513            .read_column(txn, db_ptr, line, "tag", |b| {
514                Some(String::from_utf8_lossy(b).to_string())
515            })?
516            .context("missing field tag")?;
517        let tx_ctx = self
518            .read_column(txn, db_ptr, line, "tx_ctx", |b| {
519                postcard::from_bytes(b).ok()
520            })?
521            .context("missing field tx-ctx")?;
522        let entry = LedgerEntry {
523            creditor,
524            debitor,
525            amount_milli,
526            tax_milli,
527            currency,
528            kind,
529            tag: tag.to_string(),
530        };
531        Ok(Some((entry, contract_id, tx_ctx)))
532    }
533}
534
535fn i64_from_slice(slice: &[u8]) -> Option<i64> {
536    let b = slice.try_into().ok()?;
537    Some(i64::from_be_bytes(b))
538}
539
540/// A ledger-entry meant to be consumed by APIs
541#[derive(Serialize)]
542pub struct LedgerEntryDto {
543    pub creditor: BorderlessId,
544    pub debitor: BorderlessId,
545    pub amount: String,
546    pub tax: String,
547    pub kind: String,
548    pub tag: String,
549    pub contract_id: ContractId,
550    pub tx_ctx: TxCtx,
551}
552
553impl LedgerEntryDto {
554    pub fn new(entry: LedgerEntry, contract_id: ContractId, tx_ctx: TxCtx) -> LedgerEntryDto {
555        let amount = Money::from_milli(entry.currency, entry.amount_milli).to_string();
556        let tax = Money::from_milli(entry.currency, entry.tax_milli).to_string();
557        LedgerEntryDto {
558            creditor: entry.creditor,
559            debitor: entry.debitor,
560            amount,
561            tax,
562            kind: entry.kind.to_string(),
563            tag: entry.tag,
564            contract_id,
565            tx_ctx,
566        }
567    }
568}
569
570#[derive(Serialize)]
571pub struct LedgerIds {
572    pub creditor: BorderlessId,
573    pub debitor: BorderlessId,
574    pub ledger_id: u64,
575}
576
577/// Meta information about this ledger
578#[derive(Debug, Serialize, Deserialize)]
579pub struct LedgerMeta {
580    /// Creditor side
581    pub creditor: BorderlessId,
582    /// Debitor side
583    pub debitor: BorderlessId,
584    /// Length of the ledger
585    pub len: u64,
586    /// Balances by currency ( values are in 1000 units, so 1€ = 1000 )
587    pub balances: HashMap<Currency, i64>,
588}
589
590/// Meta information about this ledger (DTO for JSON-APIs)
591///
592/// Contains the ledger-id, which is useful for later queries,
593/// + converts the amount
594#[derive(Serialize)]
595pub struct LedgerMetaDto {
596    /// ID that is used for this ledger
597    pub ledger_id: u64,
598    /// Creditor side
599    pub creditor: BorderlessId,
600    /// Debitor side
601    pub debitor: BorderlessId,
602    /// Length of the ledger
603    pub len: u64,
604    /// Balances by currency ( values are normalized, so 1€ = 1.00 €)
605    pub balances: HashMap<Currency, f64>,
606    #[serde(default)]
607    #[serde(skip_serializing_if = "Option::is_none")]
608    /// (Optional) Contract-ID, if the query was for a single contract-id only.
609    pub contract_id: Option<ContractId>,
610}
611
612impl LedgerMeta {
613    pub fn new(creditor: BorderlessId, debitor: BorderlessId) -> Self {
614        LedgerMeta {
615            creditor,
616            debitor,
617            len: 0,
618            balances: HashMap::new(),
619        }
620    }
621
622    pub fn into_dto(self) -> LedgerMetaDto {
623        let ledger_id = self.creditor.merge_compact(&self.debitor);
624        let balances = self
625            .balances
626            .into_iter()
627            .map(|(k, v)| (k, v as f64 / 1000.0))
628            .collect();
629        LedgerMetaDto {
630            ledger_id,
631            creditor: self.creditor,
632            debitor: self.debitor,
633            len: self.len,
634            balances,
635            contract_id: None,
636        }
637    }
638
639    /// Resets the balances and length - useful when creating a temporary balance from the original `LedgerMeta`
640    pub fn reset_balance(&mut self) {
641        self.balances.clear();
642        self.len = 0;
643    }
644
645    /// Updates the ledger meta information with the current entry
646    ///
647    /// Returns an error, if the ledger-entry does not belong to this ledger.
648    pub fn update(&mut self, entry: &LedgerEntry) -> Result<()> {
649        let balance = self.balances.entry(entry.currency).or_default();
650
651        // We have to modify the balance, based on the 'direction' of the transfer
652        let mul = if (entry.creditor, entry.debitor) == (self.creditor, self.debitor) {
653            /* same direction */
654            1
655        } else if (entry.creditor, entry.debitor) == (self.debitor, self.creditor) {
656            /* inverse direction */
657            -1
658        } else {
659            return Err(Error::msg("ledger-entry does not match ledger owners"));
660        };
661        match entry.kind {
662            EntryType::CREATE => {
663                *balance += mul * entry.amount_milli;
664            }
665            EntryType::SETTLE | EntryType::CANCEL => {
666                *balance -= mul * entry.amount_milli;
667            }
668        }
669        self.len += 1;
670        Ok(())
671    }
672}
673
674/// A 24-bit ledger key constructed from a pair of borderless-ids, a line-index and a 'column' name.
675///
676/// The column name is used to have different keys for different values.
677/// To be able to access the ledger with maximum speed, we do not save a single struct in one line
678/// and use a serialization format. Instead we save each value individually by its 'column name'.
679/// This allows us to scan through the key-range very efficiently.
680///
681/// The encoding of the ledger-key is as follows:
682/// ```text
683/// [ participant-pair | number | value ]
684/// [     u64          |  u64   | u64   ]
685/// ```
686struct LedgerKey([u8; 24]);
687
688impl LedgerKey {
689    /// Constructs a new ledger-key for a specific ledger-id, line and column
690    ///
691    /// The ledger-id is the calculated from the participant-ids by using `BorderlessId::merge_compact`
692    pub fn new(ledger_id: u64, line_idx: u64, column: &'static str) -> Self {
693        let participant_key = ledger_id.to_be_bytes();
694        let line_key = line_idx.to_be_bytes();
695        let column_key = xxhash_rust::const_xxh3::xxh3_64(column.as_bytes()).to_be_bytes();
696        let mut key = [0; 24];
697        key[0..8].copy_from_slice(&participant_key);
698        key[8..16].copy_from_slice(&line_key);
699        key[16..24].copy_from_slice(&column_key);
700        LedgerKey(key)
701    }
702
703    /// Returns the key, where the length of the ledger is saved
704    pub fn meta(ledger_id: u64) -> Self {
705        let participant_key = ledger_id.to_be_bytes();
706        // We want the meta-info to be the absolute last key, so we do some bit-hacking here:
707        let mut key = [0xff; 24];
708        key[0..8].copy_from_slice(&participant_key);
709        LedgerKey(key)
710    }
711
712    /// Creates a ledger-key from a slice (useful when iterating over the kv-store)
713    pub fn from_slice(slice: &[u8]) -> Self {
714        let mut key = [0; 24];
715        key[..].copy_from_slice(slice);
716        LedgerKey(key)
717    }
718
719    /// Reconstructs the 'line' (index) from the ledger-key
720    pub fn line(&self) -> u64 {
721        let mut out = [0; 8];
722        out.copy_from_slice(&self.0[8..16]);
723        u64::from_be_bytes(out)
724    }
725
726    /// Reconstructs the ledger-id from the ledger-key
727    pub fn ledger_id(&self) -> u64 {
728        let mut out = [0; 8];
729        out.copy_from_slice(&self.0[0..8]);
730        u64::from_be_bytes(out)
731    }
732
733    pub fn is_meta(&self) -> bool {
734        self.line() == u64::MAX
735    }
736}
737
738impl TryFrom<&[u8]> for LedgerKey {
739    type Error = TryFromSliceError;
740
741    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
742        let buf = value.try_into()?;
743        Ok(LedgerKey(buf))
744    }
745}
746
747impl AsRef<[u8]> for LedgerKey {
748    fn as_ref(&self) -> &[u8] {
749        &self.0
750    }
751}
752
753// #[cfg(test)]
754// mod tests {
755//     use super::*;
756// }