From 7f180d5a470f1d1051d03e2c6fc5ff9ed93b7d13 Mon Sep 17 00:00:00 2001 From: ares <285551516+New1Direction@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:50:31 -0700 Subject: [PATCH] =?UTF-8?q?refactor(ledger):=20extract=20korg-ledger=20?= =?UTF-8?q?=E2=80=94=20a=20publishable,=20single-source=20spec=20crate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the korg-ledger@v1 hash-chain primitives (canonicalize / chain_hash / verify_chain / verify_dag) out of korg-registry into a new standalone crate, korg-ledger — so the verifier publishes to crates.io as a clean `cargo install korg-verify`, and the spec implementation has one home rather than a copy per consumer. - New crate crates/korg-ledger (deps: serde_json, sha2, hmac only). The chain code moved verbatim — byte-identical (same SHA) — so conformance is unchanged. - korg-registry::ledger_chain is now a re-export of korg-ledger, so every `crate::ledger_chain::…` path (log.rs et al.) is untouched. Single source of truth, no duplicated implementation to drift. - korg-verify depends on korg-ledger instead of korg-registry (one import). Severs its only path dep on the internal runtime → publishable standalone. Tests: korg-ledger 3, korg-registry 23, korg-verify 15 (incl. the 5 frozen conformance vectors + 6 receipt tests) all pass. clippy clean, fmt clean. `cargo publish --dry-run -p korg-ledger` packages + verifies green. Publish order (needs a crates.io token): korg-ledger first, then korg-verify. --- Cargo.lock | 12 +- Cargo.toml | 1 + crates/korg-ledger/Cargo.toml | 16 ++ crates/korg-ledger/src/lib.rs | 240 ++++++++++++++++++++++ crates/korg-registry/Cargo.toml | 3 +- crates/korg-registry/src/ledger_chain.rs | 244 +---------------------- crates/korg-verify/Cargo.toml | 2 +- crates/korg-verify/README.md | 2 +- crates/korg-verify/src/lib.rs | 4 +- 9 files changed, 280 insertions(+), 244 deletions(-) create mode 100644 crates/korg-ledger/Cargo.toml create mode 100644 crates/korg-ledger/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1d9507c..a76796d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2090,6 +2090,15 @@ dependencies = [ "tokenizers", ] +[[package]] +name = "korg-ledger" +version = "0.1.0" +dependencies = [ + "hmac", + "serde_json", + "sha2", +] + [[package]] name = "korg-llm" version = "0.1.0" @@ -2120,6 +2129,7 @@ dependencies = [ "fs2", "hmac", "korg-core", + "korg-ledger", "serde", "serde_json", "sha2", @@ -2214,7 +2224,7 @@ version = "0.1.0" dependencies = [ "ed25519-dalek", "hex", - "korg-registry", + "korg-ledger", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 08c974e..96b4513 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/korg-auth", "crates/korg-llm", "crates/korg-embeddings", + "crates/korg-ledger", "crates/korg-registry", "crates/korg-runtime", "crates/korg-tui", diff --git a/crates/korg-ledger/Cargo.toml b/crates/korg-ledger/Cargo.toml new file mode 100644 index 0000000..6d18332 --- /dev/null +++ b/crates/korg-ledger/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "korg-ledger" +version = "0.1.0" +edition = "2021" +description = "korg-ledger@v1 — the tamper-evident hash-chain spec implementation (canonicalize, chain_hash, verify_chain, verify_dag). Byte-identical to the Python and JS references; SHA-256 or HMAC-keyed." +license = "MIT OR Apache-2.0" +repository = "https://github.com/New1Direction/korg" +homepage = "https://github.com/New1Direction/korg" +keywords = ["ledger", "hash-chain", "tamper-evident", "audit", "verification"] +categories = ["cryptography", "data-structures"] +readme = false + +[dependencies] +serde_json = { workspace = true } +sha2 = "0.10" +hmac = "0.12" diff --git a/crates/korg-ledger/src/lib.rs b/crates/korg-ledger/src/lib.rs new file mode 100644 index 0000000..97c1111 --- /dev/null +++ b/crates/korg-ledger/src/lib.rs @@ -0,0 +1,240 @@ +//! korg-ledger@v1 — tamper-evident hash-chain (Rust reference implementation). +//! +//! The Rust implementation of the frozen `korg-ledger@v1` spec. It produces +//! byte-identical hashes to the Python and JS references; that cross-language +//! equivalence is pinned by frozen conformance vectors (see the `korg-verify` +//! and `korg-registry` conformance tests, which assert tip hashes computed by +//! the Python reference). +//! +//! Guarantee: a sequence of events is hash-chained — each carries `prev_hash` +//! (the previous event's `entry_hash`, GENESIS for the first) and `entry_hash` +//! (hash of its own canonical preimage). Any edit/delete/insert/reorder breaks +//! the chain and is localized to a `seq_id`. With an HMAC key the chain is +//! tamper-PROOF (unforgeable without the key), not merely tamper-evident. + +use hmac::{Hmac, Mac}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +/// The chain anchor: `prev_hash` of the first event in a journal (64 zero hex chars). +pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; + +/// Fields that ARE the hash/signature and so are excluded from the preimage. +const HASH_FIELDS: &[&str] = &["entry_hash"]; + +/// Canonical byte encoding of a JSON value (korg-ledger@v1 §2). +/// +/// Reproduces Python `json.dumps(value, sort_keys=True, separators=(",",":"))` +/// with the default `ensure_ascii=True`: +/// - object keys sorted ascending by code point (UTF-8 byte order == code +/// point order for valid UTF-8); +/// - no insignificant whitespace; +/// - non-ASCII escaped as `\uXXXX` (lowercase), so output is pure ASCII. +pub fn canonicalize(value: &Value) -> Vec { + let mut s = String::new(); + write_canonical(value, &mut s); + s.into_bytes() +} + +fn write_canonical(v: &Value, out: &mut String) { + match v { + Value::Null => out.push_str("null"), + Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }), + Value::Number(n) => out.push_str(&n.to_string()), + Value::String(s) => write_json_string(s, out), + Value::Array(arr) => { + out.push('['); + for (i, e) in arr.iter().enumerate() { + if i > 0 { + out.push(','); + } + write_canonical(e, out); + } + out.push(']'); + } + Value::Object(map) => { + let mut keys: Vec<&String> = map.keys().collect(); + keys.sort(); // lexicographic by code point + out.push('{'); + for (i, k) in keys.iter().enumerate() { + if i > 0 { + out.push(','); + } + write_json_string(k, out); + out.push(':'); + write_canonical(&map[*k], out); + } + out.push('}'); + } + } +} + +/// Escape a string exactly as Python's `json.dumps(..., ensure_ascii=True)`: +/// short escapes for the standard controls, `\uXXXX` for everything outside the +/// printable-ASCII range `0x20..=0x7e` (surrogate pairs above U+FFFF). +fn write_json_string(s: &str, out: &mut String) { + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + '\u{08}' => out.push_str("\\b"), + '\u{0c}' => out.push_str("\\f"), + c if ('\u{20}'..='\u{7e}').contains(&c) => out.push(c), + c => { + let cp = c as u32; + if cp > 0xFFFF { + let v = cp - 0x10000; + out.push_str(&format!("\\u{:04x}", 0xD800 + (v >> 10))); + out.push_str(&format!("\\u{:04x}", 0xDC00 + (v & 0x3FF))); + } else { + out.push_str(&format!("\\u{:04x}", cp)); + } + } + } + } + out.push('"'); +} + +fn hex_lower(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{:02x}", b)); + } + s +} + +/// Compute an event's `entry_hash` (korg-ledger@v1 §3). +/// +/// Preimage = canonical encoding of the event with its hash field(s) removed +/// (`prev_hash` is kept — that's the chain link). With `key`, HMAC-SHA256; +/// otherwise SHA-256. Returns lowercase hex. +pub fn chain_hash(event: &Value, key: Option<&[u8]>) -> String { + let mut obj = event.as_object().cloned().unwrap_or_default(); + for f in HASH_FIELDS { + obj.remove(*f); + } + let data = canonicalize(&Value::Object(obj)); + match key { + Some(k) => { + let mut mac = + Hmac::::new_from_slice(k).expect("HMAC accepts keys of any length"); + mac.update(&data); + hex_lower(&mac.finalize().into_bytes()) + } + None => { + let mut h = Sha256::new(); + h.update(&data); + hex_lower(&h.finalize()) + } + } +} + +/// Recompute the hash-chain and report tampering (korg-ledger@v1 §5). +/// Returns an empty vec iff the chain is intact; each error names a `seq_id`. +pub fn verify_chain(events: &[Value], key: Option<&[u8]>) -> Vec { + let mut errors = Vec::new(); + let mut expected_prev = GENESIS_HASH.to_string(); + for e in events { + let sid = e + .get("seq_id") + .map(|v| v.to_string()) + .unwrap_or_else(|| "?".to_string()); + let stored = e.get("entry_hash").and_then(|v| v.as_str()); + match stored { + None => { + errors.push(format!( + "seq {sid}: missing entry_hash (event is not chained)" + )); + // sentinel that cannot equal any real 64-hex hash → next link fails + expected_prev = String::new(); + } + Some(stored) => { + let prev = e.get("prev_hash").and_then(|v| v.as_str()).unwrap_or(""); + if prev != expected_prev { + errors.push(format!( + "seq {sid}: prev_hash breaks the chain \ + (an event was inserted, deleted, or reordered)" + )); + } + if chain_hash(e, key) != stored { + errors.push(format!( + "seq {sid}: entry_hash mismatch (content was tampered)" + )); + } + expected_prev = stored.to_string(); + } + } + } + errors +} + +/// Check the causal DAG is well-formed (korg-ledger@v1 §5): unique `seq_id`s, +/// and every `triggered_by` references an existing, strictly-earlier `seq_id`. +pub fn verify_dag(events: &[Value]) -> Vec { + let mut errors = Vec::new(); + let seqs: Vec = events + .iter() + .filter_map(|e| e.get("seq_id").and_then(|v| v.as_i64())) + .collect(); + let seqset: std::collections::HashSet = seqs.iter().copied().collect(); + if seqset.len() != seqs.len() { + errors.push("duplicate seq_id present".to_string()); + } + for e in events { + let tb = match e.get("triggered_by").and_then(|v| v.as_i64()) { + Some(tb) => tb, + None => continue, + }; + let sid = e.get("seq_id").and_then(|v| v.as_i64()); + if !seqset.contains(&tb) { + errors.push(format!("seq {sid:?}: triggered_by {tb} does not exist")); + } else if let Some(sid) = sid { + if tb >= sid { + errors.push(format!( + "seq {sid}: triggered_by {tb} is not strictly earlier" + )); + } + } + } + errors +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn canonicalize_sorts_keys_and_is_compact() { + assert_eq!( + canonicalize(&json!({"z":[3,2],"a":{"y":1,"x":2}})), + b"{\"a\":{\"x\":2,\"y\":1},\"z\":[3,2]}" + ); + } + + #[test] + fn canonicalize_escapes_non_ascii() { + // matches Python ensure_ascii: é → é + assert_eq!( + canonicalize(&json!({"a":"é"})), + b"{\"a\":\"\\u00e9\"}".to_vec() + ); + } + + #[test] + fn entry_hash_excludes_itself_but_keeps_prev_hash() { + let ev = json!({"seq_id":1,"tool_name":"x","prev_hash":GENESIS_HASH}); + let h = chain_hash(&ev, None); + let mut with = ev.clone(); + with["entry_hash"] = json!("anything"); + assert_eq!(chain_hash(&with, None), h); + // changing prev_hash DOES change the hash (it's part of the preimage) + let mut other = ev.clone(); + other["prev_hash"] = json!("ff"); + assert_ne!(chain_hash(&other, None), h); + } +} diff --git a/crates/korg-registry/Cargo.toml b/crates/korg-registry/Cargo.toml index 4ad9bdc..66b517b 100644 --- a/crates/korg-registry/Cargo.toml +++ b/crates/korg-registry/Cargo.toml @@ -6,7 +6,8 @@ description = "Capability journal, ledger, and plan execution for korg" license = "MIT OR Apache-2.0" [dependencies] -korg-core = { path = "../korg-core" } +korg-core = { path = "../korg-core" } +korg-ledger = { path = "../korg-ledger", version = "0.1.0" } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } diff --git a/crates/korg-registry/src/ledger_chain.rs b/crates/korg-registry/src/ledger_chain.rs index 6ee9fa9..7050b2a 100644 --- a/crates/korg-registry/src/ledger_chain.rs +++ b/crates/korg-registry/src/ledger_chain.rs @@ -1,240 +1,8 @@ -//! korg-ledger@v1 — tamper-evident hash-chain (Rust reference). +//! Re-export of the standalone [`korg_ledger`] crate (the korg-ledger@v1 spec impl). //! -//! This is the Rust implementation of the frozen `korg-ledger@v1` spec -//! (canonical text: korgex `spec/korg-ledger-v1/SPEC.md`, vendored under -//! `tests/conformance/`). It MUST produce byte-identical hashes to the Python -//! reference; that equivalence is proven by the conformance vectors in -//! `tests/conformance.rs`, which pin frozen tip hashes computed by Python. -//! -//! Guarantee: a sequence of events is hash-chained — each carries `prev_hash` -//! (the previous event's `entry_hash`, GENESIS for the first) and `entry_hash` -//! (hash of its own canonical preimage). Any edit/delete/insert/reorder breaks -//! the chain and is localized to a `seq_id`. With an HMAC key the chain is -//! tamper-PROOF (unforgeable without the key), not merely tamper-evident. - -use hmac::{Hmac, Mac}; -use serde_json::Value; -use sha2::{Digest, Sha256}; - -/// The chain anchor: `prev_hash` of the first event in a journal (64 zero hex chars). -pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; - -/// Fields that ARE the hash/signature and so are excluded from the preimage. -const HASH_FIELDS: &[&str] = &["entry_hash"]; - -/// Canonical byte encoding of a JSON value (korg-ledger@v1 §2). -/// -/// Reproduces Python `json.dumps(value, sort_keys=True, separators=(",",":"))` -/// with the default `ensure_ascii=True`: -/// - object keys sorted ascending by code point (UTF-8 byte order == code -/// point order for valid UTF-8); -/// - no insignificant whitespace; -/// - non-ASCII escaped as `\uXXXX` (lowercase), so output is pure ASCII. -pub fn canonicalize(value: &Value) -> Vec { - let mut s = String::new(); - write_canonical(value, &mut s); - s.into_bytes() -} - -fn write_canonical(v: &Value, out: &mut String) { - match v { - Value::Null => out.push_str("null"), - Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }), - Value::Number(n) => out.push_str(&n.to_string()), - Value::String(s) => write_json_string(s, out), - Value::Array(arr) => { - out.push('['); - for (i, e) in arr.iter().enumerate() { - if i > 0 { - out.push(','); - } - write_canonical(e, out); - } - out.push(']'); - } - Value::Object(map) => { - let mut keys: Vec<&String> = map.keys().collect(); - keys.sort(); // lexicographic by code point - out.push('{'); - for (i, k) in keys.iter().enumerate() { - if i > 0 { - out.push(','); - } - write_json_string(k, out); - out.push(':'); - write_canonical(&map[*k], out); - } - out.push('}'); - } - } -} - -/// Escape a string exactly as Python's `json.dumps(..., ensure_ascii=True)`: -/// short escapes for the standard controls, `\uXXXX` for everything outside the -/// printable-ASCII range `0x20..=0x7e` (surrogate pairs above U+FFFF). -fn write_json_string(s: &str, out: &mut String) { - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - '\u{08}' => out.push_str("\\b"), - '\u{0c}' => out.push_str("\\f"), - c if ('\u{20}'..='\u{7e}').contains(&c) => out.push(c), - c => { - let cp = c as u32; - if cp > 0xFFFF { - let v = cp - 0x10000; - out.push_str(&format!("\\u{:04x}", 0xD800 + (v >> 10))); - out.push_str(&format!("\\u{:04x}", 0xDC00 + (v & 0x3FF))); - } else { - out.push_str(&format!("\\u{:04x}", cp)); - } - } - } - } - out.push('"'); -} - -fn hex_lower(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 2); - for b in bytes { - s.push_str(&format!("{:02x}", b)); - } - s -} - -/// Compute an event's `entry_hash` (korg-ledger@v1 §3). -/// -/// Preimage = canonical encoding of the event with its hash field(s) removed -/// (`prev_hash` is kept — that's the chain link). With `key`, HMAC-SHA256; -/// otherwise SHA-256. Returns lowercase hex. -pub fn chain_hash(event: &Value, key: Option<&[u8]>) -> String { - let mut obj = event.as_object().cloned().unwrap_or_default(); - for f in HASH_FIELDS { - obj.remove(*f); - } - let data = canonicalize(&Value::Object(obj)); - match key { - Some(k) => { - let mut mac = - Hmac::::new_from_slice(k).expect("HMAC accepts keys of any length"); - mac.update(&data); - hex_lower(&mac.finalize().into_bytes()) - } - None => { - let mut h = Sha256::new(); - h.update(&data); - hex_lower(&h.finalize()) - } - } -} - -/// Recompute the hash-chain and report tampering (korg-ledger@v1 §5). -/// Returns an empty vec iff the chain is intact; each error names a `seq_id`. -pub fn verify_chain(events: &[Value], key: Option<&[u8]>) -> Vec { - let mut errors = Vec::new(); - let mut expected_prev = GENESIS_HASH.to_string(); - for e in events { - let sid = e - .get("seq_id") - .map(|v| v.to_string()) - .unwrap_or_else(|| "?".to_string()); - let stored = e.get("entry_hash").and_then(|v| v.as_str()); - match stored { - None => { - errors.push(format!( - "seq {sid}: missing entry_hash (event is not chained)" - )); - // sentinel that cannot equal any real 64-hex hash → next link fails - expected_prev = String::new(); - } - Some(stored) => { - let prev = e.get("prev_hash").and_then(|v| v.as_str()).unwrap_or(""); - if prev != expected_prev { - errors.push(format!( - "seq {sid}: prev_hash breaks the chain \ - (an event was inserted, deleted, or reordered)" - )); - } - if chain_hash(e, key) != stored { - errors.push(format!( - "seq {sid}: entry_hash mismatch (content was tampered)" - )); - } - expected_prev = stored.to_string(); - } - } - } - errors -} - -/// Check the causal DAG is well-formed (korg-ledger@v1 §5): unique `seq_id`s, -/// and every `triggered_by` references an existing, strictly-earlier `seq_id`. -pub fn verify_dag(events: &[Value]) -> Vec { - let mut errors = Vec::new(); - let seqs: Vec = events - .iter() - .filter_map(|e| e.get("seq_id").and_then(|v| v.as_i64())) - .collect(); - let seqset: std::collections::HashSet = seqs.iter().copied().collect(); - if seqset.len() != seqs.len() { - errors.push("duplicate seq_id present".to_string()); - } - for e in events { - let tb = match e.get("triggered_by").and_then(|v| v.as_i64()) { - Some(tb) => tb, - None => continue, - }; - let sid = e.get("seq_id").and_then(|v| v.as_i64()); - if !seqset.contains(&tb) { - errors.push(format!("seq {sid:?}: triggered_by {tb} does not exist")); - } else if let Some(sid) = sid { - if tb >= sid { - errors.push(format!( - "seq {sid}: triggered_by {tb} is not strictly earlier" - )); - } - } - } - errors -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn canonicalize_sorts_keys_and_is_compact() { - assert_eq!( - canonicalize(&json!({"z":[3,2],"a":{"y":1,"x":2}})), - b"{\"a\":{\"x\":2,\"y\":1},\"z\":[3,2]}" - ); - } - - #[test] - fn canonicalize_escapes_non_ascii() { - // matches Python ensure_ascii: é → é - assert_eq!( - canonicalize(&json!({"a":"é"})), - b"{\"a\":\"\\u00e9\"}".to_vec() - ); - } +//! The tamper-evident hash-chain primitives now live in their own publishable, +//! independently-auditable crate (`korg-ledger`). They are re-exported here so existing +//! `crate::ledger_chain::…` paths and the public `korg-registry` API stay unchanged — +//! single source of truth, no duplicated implementation to drift. - #[test] - fn entry_hash_excludes_itself_but_keeps_prev_hash() { - let ev = json!({"seq_id":1,"tool_name":"x","prev_hash":GENESIS_HASH}); - let h = chain_hash(&ev, None); - let mut with = ev.clone(); - with["entry_hash"] = json!("anything"); - assert_eq!(chain_hash(&with, None), h); - // changing prev_hash DOES change the hash (it's part of the preimage) - let mut other = ev.clone(); - other["prev_hash"] = json!("ff"); - assert_ne!(chain_hash(&other, None), h); - } -} +pub use korg_ledger::*; diff --git a/crates/korg-verify/Cargo.toml b/crates/korg-verify/Cargo.toml index ddcc185..b4ad336 100644 --- a/crates/korg-verify/Cargo.toml +++ b/crates/korg-verify/Cargo.toml @@ -15,7 +15,7 @@ name = "korg-verify" path = "src/main.rs" [dependencies] -korg-registry = { path = "../korg-registry" } +korg-ledger = { path = "../korg-ledger", version = "0.1.0" } serde_json = { workspace = true } ed25519-dalek = "2" hex = "0.4" diff --git a/crates/korg-verify/README.md b/crates/korg-verify/README.md index 4c4d942..3fff4ba 100644 --- a/crates/korg-verify/README.md +++ b/crates/korg-verify/README.md @@ -10,7 +10,7 @@ Exit code: `0` valid · `1` invalid/tampered · `2` usage/parse error. ## What it checks -- **Hash chain** — every event's `entry_hash` recomputes and links unbroken from genesis (tamper-evident). Reuses `korg-registry`'s conformance-tested `verify_chain`. +- **Hash chain** — every event's `entry_hash` recomputes and links unbroken from genesis (tamper-evident). Reuses `korg-ledger`'s conformance-tested `verify_chain`. - **Causal DAG** — `triggered_by` links are well-formed (`verify_dag`). - **Tip** — a receipt's recorded `tip` matches the chain head. - **Signature** — if the receipt is signed, the Ed25519 signature over the tip is valid. `--pubkey ` *pins* the expected signer and rejects any other key (so a green check proves authorship against a key you trust, not merely against the one the receipt carries). diff --git a/crates/korg-verify/src/lib.rs b/crates/korg-verify/src/lib.rs index 5c47cbe..0b50883 100644 --- a/crates/korg-verify/src/lib.rs +++ b/crates/korg-verify/src/lib.rs @@ -1,7 +1,7 @@ //! korg-verify — an independent, dependency-light verifier for korg receipts and //! journals. //! -//! It reuses the conformance-tested chain primitives in `korg-registry` +//! It reuses the conformance-tested chain primitives in `korg-ledger` //! (`canonicalize` / `chain_hash` / `verify_chain` / `verify_dag` — proven //! byte-identical to the Python and JS implementations against the frozen //! korg-ledger@v1 vectors) and adds the receipt envelope plus the Ed25519 @@ -15,7 +15,7 @@ //! that the key maps to a real-world identity (the relying party pins that — see //! `--pubkey`). -use korg_registry::ledger_chain::{verify_chain, verify_dag}; +use korg_ledger::{verify_chain, verify_dag}; use serde_json::Value; /// The outcome of verifying a receipt or journal. `valid` is the conjunction of every