|
| 1 | +//! Public supply audit (mirror of `opencsv-core::audit::supply`, paper |
| 2 | +//! §4.9): |
| 3 | +//! |
| 4 | +//! ```text |
| 5 | +//! supply(asset_id, h) = Σ V over MINT anchors with this asset_id up to h |
| 6 | +//! − Σ V over REDEEM anchors with this asset_id up to h |
| 7 | +//! ``` |
| 8 | +//! |
| 9 | +//! Anchor records are copyable bytes, so identical MINT anchors are |
| 10 | +//! **deduplicated**: each distinct `mint_commit` counts once per asset. |
| 11 | +
|
| 12 | +use crate::record::Record; |
| 13 | +use crate::types::{AssetId24, Location, MintCommit}; |
| 14 | + |
| 15 | +/// Failure modes of [`supply`] (mirror of `audit::SupplyError`). |
| 16 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 17 | +pub enum SupplyError { |
| 18 | + /// Redemptions exceed mints at the requested height — the anchored |
| 19 | + /// stream is inconsistent (only possible on an adversarial/corrupt |
| 20 | + /// chain view). |
| 21 | + NegativeSupply, |
| 22 | +} |
| 23 | + |
| 24 | +/// Does `seen` already contain `commit`? (Linear scan — no `HashSet` in |
| 25 | +/// the verification surface.) |
| 26 | +fn seen_contains(seen: &[MintCommit], commit: &MintCommit) -> bool { |
| 27 | + let mut i = 0usize; |
| 28 | + while i < seen.len() { |
| 29 | + if seen[i] == *commit { |
| 30 | + return true; |
| 31 | + } |
| 32 | + i += 1; |
| 33 | + } |
| 34 | + false |
| 35 | +} |
| 36 | + |
| 37 | +/// Compute the public per-asset supply at `height` (paper §4.9), over the |
| 38 | +/// anchor records with location at or below `height`, in canonical order. |
| 39 | +/// |
| 40 | +/// Mirror of `audit::supply`: MINT records with a matching asset count |
| 41 | +/// once per distinct `mint_commit`; REDEEM records with a matching asset |
| 42 | +/// subtract; everything else is ignored. Fails with |
| 43 | +/// [`SupplyError::NegativeSupply`] if redemptions exceed mints. |
| 44 | +pub fn supply( |
| 45 | + anchors: &[(Location, Record)], |
| 46 | + asset_id: &AssetId24, |
| 47 | + height: u64, |
| 48 | +) -> Result<u64, SupplyError> { |
| 49 | + let mut seen_mints: Vec<MintCommit> = Vec::new(); |
| 50 | + let mut total: i128 = 0; |
| 51 | + let mut i = 0usize; |
| 52 | + while i < anchors.len() { |
| 53 | + let (location, record) = &anchors[i]; |
| 54 | + if location.height <= height { |
| 55 | + // Note: match by value (Aeneas chokes on by-reference matches |
| 56 | + // inside loops); `Record` is `Copy`, so this is free. |
| 57 | + match *record { |
| 58 | + Record::Mint { |
| 59 | + asset_id: record_asset, |
| 60 | + value, |
| 61 | + mint_commit, |
| 62 | + } => { |
| 63 | + if record_asset == *asset_id && !seen_contains(&seen_mints, &mint_commit) { |
| 64 | + seen_mints.push(mint_commit); |
| 65 | + total += i128::from(value); |
| 66 | + } |
| 67 | + } |
| 68 | + Record::Redeem { |
| 69 | + asset_id: record_asset, |
| 70 | + value, |
| 71 | + .. |
| 72 | + } => { |
| 73 | + if record_asset == *asset_id { |
| 74 | + total -= i128::from(value); |
| 75 | + } |
| 76 | + } |
| 77 | + _ => {} |
| 78 | + } |
| 79 | + } |
| 80 | + i += 1; |
| 81 | + } |
| 82 | + if total < 0 || total > i128::from(u64::MAX) { |
| 83 | + Err(SupplyError::NegativeSupply) |
| 84 | + } else { |
| 85 | + Ok(total as u64) |
| 86 | + } |
| 87 | +} |
0 commit comments