Skip to content

Commit b64bdf4

Browse files
committed
kernel: pure decision-logic crate for Aeneas verification
Carve the pure decision logic out of opencsv-core into a new opencsv-kernel crate, shaped for the Aeneas toolchain: - loops only (no iterators/closures in the verified paths), no serde, no dyn, no RNG - hashing behind an opaque boundary (hash::hash_bind/hash_batch delegate byte-identically to opencsv-core's anchor::binding/batch::batch_commit) - interop module (excluded from translation) converts core types to kernel types Equivalence with opencsv-core is pinned by tests/kernel_equiv.rs (8 tests: binding byte-identity, well_formed on all variants, batch occurrence, first-occurrence double-spend/copy-grief, supply mint/redeem accounting). charon --preset=aeneas --opaque opencsv_kernel::hash --exclude opencsv_kernel::interop translates the crate with no errors, and aeneas generates Lean that builds; the by-value match on *record inside the supply loop is the one Aeneas-driven idiom (by-reference matches in loops are unsupported).
1 parent 1616397 commit b64bdf4

13 files changed

Lines changed: 756 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
resolver = "2"
33
members = [
44
"crates/opencsv-core",
5+
"crates/opencsv-kernel",
56
"crates/opencsv-pcd",
67
"crates/opencsv-cli",
78
"crates/opencsv-signal",

crates/opencsv-kernel/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "opencsv-kernel"
3+
description = "Pure decision-logic kernel of opencsv-core, written for Aeneas verification (loops only, no serde/dyn/RNG, hash behind an opaque boundary)"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
8+
[dependencies]
9+
opencsv-core = { path = "../opencsv-core" }
10+
11+
[dev-dependencies]
12+
13+
[features]
14+
default = []

crates/opencsv-kernel/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# opencsv-kernel
2+
3+
The **pure decision logic** of `opencsv-core`, rewritten in the shape the
4+
Aeneas spike validated for Rust→Lean 4 translation:
5+
6+
- **loops only** — no iterator adapters (`zip` / `chunks` / `enumerate` /
7+
`position` / `filter` / `map`): those trip charon's associated-type
8+
lifting and have no model in aeneas's Lean library;
9+
- **no serde, no `dyn` traits, no RNG, no generics** beyond plain numeric /
10+
byte-array types;
11+
- the Poseidon hash stays behind an **opaque boundary** (`hash` module) —
12+
the same cryptographic boundary the Lean model
13+
(`formal/OpenCsv/Interfaces.lean: bindHash`) takes as an axiom.
14+
15+
## Scope (phase 1)
16+
17+
| kernel item | mirrors (opencsv-core) |
18+
|---|---|
19+
| `binding::binding` | `anchor::binding` + `Digest::to_anchor` |
20+
| `record::Record::well_formed` | `anchor::AnchorRecord::well_formed` (+ `payload_slots`) |
21+
| `scan::first_occurrence` | `chain::AnchorChain::first_nullifier_occurrence` (mock semantics) |
22+
| `batch::batch_occurrence` | `batch::envelope_occurrence` |
23+
| `audit::supply` | `audit::supply` (mint dedupe by `mint_commit`) |
24+
25+
Semantics are **byte-identical** to `opencsv-core` — this is a rewrite, not
26+
a redesign. `tests/kernel_equiv.rs` ports the relevant `opencsv-core` test
27+
scenarios and asserts kernel ≡ core on shared cases.
28+
29+
## Boundary
30+
31+
- `types`, `binding`, `record`, `scan`, `batch`, `audit` are the
32+
**verification surface**: plain data in, plain data out.
33+
- `hash` is the **crypto boundary**: the only place the kernel calls into
34+
`opencsv-core` (`anchor::binding`, `batch::batch_commit`). For the
35+
Aeneas run it is translated as an opaque (uninterpreted) function —
36+
exactly the model's `bindHash` axiom.
37+
- `interop` (conversions from `opencsv-core` types) is glue for callers and
38+
tests; it is **not** part of the verification surface (excluded from
39+
translation).
40+
- `scan::first_occurrence` takes the entries in **canonical chain order**
41+
(block height, then in-block position — the caller's responsibility,
42+
same contract as `AnchorChain`) and returns the *index* of the first
43+
well-formed entry; the entry's `Location` is read off the input. The
44+
core returns the location directly — equivalent by construction.
45+
46+
Wiring the kernel into `opencsv-core` (replacing the duplicated logic) is a
47+
later decision; for now the two coexist and the equivalence tests pin them
48+
together.

crates/opencsv-kernel/src/audit.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
}

crates/opencsv-kernel/src/batch.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//! Batch occurrence test (mirror of `opencsv-core::batch::envelope_occurrence`).
2+
3+
use crate::binding::{binding, truncate24};
4+
use crate::hash;
5+
use crate::types::{Ctx, Payload, RawNf};
6+
7+
/// Is `raw_nf` an occurrence of this batch? Returns the envelope index of
8+
/// its payload (mirror of `batch::envelope_occurrence`):
9+
///
10+
/// - the envelope must carry exactly `count` payloads;
11+
/// - the header's `batch_commit` must recompute over the envelope
12+
/// (`H("batch" ∥ P_1 ∥ … ∥ P_n ∥ ctx)`, truncated);
13+
/// - some payload must equal `H("bind" ∥ raw_nf ∥ ctx)` (truncated).
14+
///
15+
/// `count` and `batch_commit` come from the batch header record of the
16+
/// transaction the envelope was taken from; `ctx` is the transaction's
17+
/// funding ctx. Loop-based (Aeneas-compatible shape).
18+
pub fn batch_occurrence(
19+
count: u8,
20+
batch_commit: &Payload,
21+
envelope: &[Payload],
22+
ctx: &Ctx,
23+
raw_nf: &RawNf,
24+
) -> Option<u32> {
25+
if envelope.len() != count as usize {
26+
return None;
27+
}
28+
let committed = truncate24(&hash::hash_batch(envelope, ctx));
29+
if committed != *batch_commit {
30+
return None;
31+
}
32+
let bound = binding(raw_nf, ctx);
33+
let mut i = 0usize;
34+
while i < envelope.len() {
35+
if envelope[i] == bound {
36+
return Some(i as u32);
37+
}
38+
i += 1;
39+
}
40+
None
41+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! The transaction-context binding (mirror of `opencsv-core::anchor`).
2+
3+
use crate::hash;
4+
use crate::types::{Ctx, Payload, RawNf};
5+
6+
/// The 24-byte anchor-carrying prefix of a digest (`Digest::to_anchor`).
7+
pub fn truncate24(digest: &[u8; 32]) -> Payload {
8+
let mut out = [0u8; 24];
9+
let mut i = 0usize;
10+
while i < 24 {
11+
out[i] = digest[i];
12+
i += 1;
13+
}
14+
out
15+
}
16+
17+
/// `P = H("bind" ∥ raw ∥ ctx)` truncated to the on-chain payload
18+
/// (`opencsv_core::anchor::binding` + `Digest::to_anchor`).
19+
pub fn binding(raw: &RawNf, ctx: &Ctx) -> Payload {
20+
let digest = hash::hash_bind(raw, ctx);
21+
truncate24(&digest)
22+
}

crates/opencsv-kernel/src/hash.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//! The cryptographic boundary of the kernel: the only functions that call
2+
//! into the scheme's Poseidon2 hash (via `opencsv-core`). For the Aeneas
3+
//! translation these are marked **opaque** — the Lean side sees them as
4+
//! uninterpreted functions, exactly the model's `bindHash` axiom
5+
//! (`formal/OpenCsv/Interfaces.lean`).
6+
//!
7+
//! These wrappers are byte-identical delegations, NOT reimplementations.
8+
9+
use crate::types::{Ctx, Payload, RawNf};
10+
11+
/// `H("bind" ∥ raw_nf ∥ ctx)` as a full 32-byte digest
12+
/// (`opencsv_core::anchor::binding`).
13+
pub fn hash_bind(raw_nf: &RawNf, ctx: &Ctx) -> [u8; 32] {
14+
*opencsv_core::anchor::binding(
15+
&opencsv_core::Digest::from_bytes(*raw_nf),
16+
ctx,
17+
)
18+
.as_bytes()
19+
}
20+
21+
/// `H("batch" ∥ P_1 ∥ … ∥ P_n ∥ ctx)` as a full 32-byte digest
22+
/// (`opencsv_core::batch::batch_commit`).
23+
pub fn hash_batch(payloads: &[Payload], ctx: &Ctx) -> [u8; 32] {
24+
let truncated: Vec<opencsv_core::TruncatedDigest> = payloads
25+
.iter()
26+
.map(|p| opencsv_core::TruncatedDigest(*p))
27+
.collect();
28+
*opencsv_core::batch::batch_commit(&truncated, ctx).as_bytes()
29+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Conversions from `opencsv-core` types to kernel types — glue for
2+
//! callers and the equivalence tests. **Not** part of the verification
3+
//! surface (excluded from the Aeneas translation; see crate README).
4+
5+
use opencsv_core::anchor::AnchorRecord;
6+
use opencsv_core::chain::{AnchorChain, AnchorLocation, AnchorRef};
7+
8+
use crate::record::Record;
9+
use crate::types::{Entry, Location};
10+
11+
/// Convert a location.
12+
pub fn location(location: &AnchorLocation) -> Location {
13+
Location {
14+
height: location.height,
15+
position: location.position,
16+
}
17+
}
18+
19+
/// Convert an anchor record (layout-compatible; see `crate::record`).
20+
pub fn record(record: &AnchorRecord) -> Record {
21+
match record {
22+
AnchorRecord::Mint {
23+
asset_id,
24+
value,
25+
mint_commit,
26+
} => Record::Mint {
27+
asset_id: asset_id.0,
28+
value: *value,
29+
mint_commit: mint_commit.0,
30+
},
31+
AnchorRecord::Xfer { payloads } => Record::Xfer {
32+
payloads: [payloads[0].0, payloads[1].0],
33+
},
34+
AnchorRecord::XferCompressed {
35+
nullifier_commit, ..
36+
} => Record::XferCompressed {
37+
payload: nullifier_commit.0,
38+
},
39+
AnchorRecord::BatchHeader {
40+
count,
41+
batch_commit,
42+
} => Record::BatchHeader {
43+
count: *count,
44+
batch_commit: batch_commit.0,
45+
},
46+
AnchorRecord::Redeem {
47+
asset_id,
48+
value,
49+
payload,
50+
} => Record::Redeem {
51+
asset_id: asset_id.0,
52+
value: *value,
53+
payload: payload.0,
54+
},
55+
}
56+
}
57+
58+
/// Resolve an anchor reference to a kernel entry (record, ctx, location),
59+
/// using the chain's `anchor_at` / `ctx_at` lookups. Returns `None` if
60+
/// either lookup fails.
61+
pub fn entry_at<C: AnchorChain>(chain: &C, anchor_ref: &AnchorRef) -> Option<Entry> {
62+
let record = chain.anchor_at(anchor_ref)?;
63+
let ctx = chain.ctx_at(anchor_ref)?;
64+
Some(Entry {
65+
record: self::record(&record),
66+
ctx,
67+
location: location(&anchor_ref.location),
68+
})
69+
}

crates/opencsv-kernel/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Pure decision-logic kernel of opencsv-core (see crate README).
2+
//!
3+
//! The **verification surface** — `types`, `binding`, `record`, `scan`,
4+
//! `batch`, `audit` — is written for Aeneas translation: loops only, no
5+
//! serde/dyn/RNG, no generics beyond plain byte arrays and integers. The
6+
//! `hash` module is the cryptographic boundary (translated as opaque).
7+
//! `interop` is glue outside the verification surface.
8+
9+
pub mod audit;
10+
pub mod batch;
11+
pub mod binding;
12+
pub mod hash;
13+
pub mod interop;
14+
pub mod record;
15+
pub mod scan;
16+
pub mod types;
17+
18+
pub use audit::{supply, SupplyError};
19+
pub use batch::batch_occurrence;
20+
pub use binding::{binding, truncate24};
21+
pub use record::Record;
22+
pub use scan::first_occurrence;
23+
pub use types::{AssetId24, Ctx, Entry, Location, MintCommit, Payload, RawNf};

0 commit comments

Comments
 (0)