From 4846f2125ab763be2140ed292241cdd08c0931da Mon Sep 17 00:00:00 2001 From: "Claude (Emil)" Date: Fri, 9 Oct 2026 17:29:11 +0000 Subject: [PATCH 1/2] Close a key in every namespace held: one change-set per namespace, one invocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #151 (PRD issue 8, §3.2, §3.3; rulings 47 and 69; AC-47 third bullet). `ledger identity revoke|rotate --everywhere` closes the key in every namespace its principal holds it open in: one change-set per namespace, each the same act closing that namespace's binding with the same `at`, signed in its own namespace, filed namespace by namespace and gated as it lands, naming every file written, to land in one commit (LP-6.34). Without the flag a close ends the key in the namespace of the binding it names, as before, and the split is a notice. A key already closed everywhere is refused with nothing to close. The other half of this issue — the writer opening every namespace on its own genesis, roles, first policy and self-bound binding, `join_genesis` gone, `--external-ref` required every time — landed with issues 5 and 6, for the reason their commits give; the `genesis_key.rs` rewrites §3.11 lists landed with issue 6. `--without-key` stays until issue 13. Tests (each failing before the change): `key_across_namespaces.rs` — a revoke everywhere files one change-set in A and one in B, both closes with one `at`, each naming its namespace's binding, named in the verb's output, landing in one commit; `verify` is conformant with no split notice and both `allowed_signers` lines end; closed everywhere, a second `--everywhere` is refused; a rotate everywhere closes the key and binds the new one in each namespace, the new key signs in B and the old one does not; and the carried test now drives `--everywhere` too: no agent identity and no non-interactive session produces a key close. Also: `signers_per_namespace.rs` (issue 7) pauses a second after opening its two namespaces, as its sibling key tests do — a close in the same second as the policies it closes under sat at their second for `ssh-keygen`, and the test was flaky. Protocol: LP-6.34 (new), LP-6.32 loses its note; Appendix B row; no Appendix C note (the issue row says none). CLAUDE.md updated. No class added; no digest moves. Stacked on #161 (issue 7). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GgRcHXmnAu43hChNxhnqxU --- CLAUDE.md | 6 +- ledger-cli/src/commands/authority.rs | 6 +- ledger-cli/src/commands/authority_enum.rs | 8 ++ ledger-cli/tests/key_across_namespaces.rs | 94 ++++++++++++++++++++--- ledger-core/src/author/identity_ops.rs | 64 ++++++++++++++- ledger/spec/ledger-protocol.md | 4 +- 6 files changed, 163 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8eecace2..df6a67a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -340,8 +340,10 @@ store (`.decisions/`), separate ontology. namespace (`A003`/`A005` count one trust root each). Keys are per namespace too (ruling 47): `init --namespace` self-binds the holder's configured key in every namespace it opens, a close ends a key in its - own namespace only, and a key closed in one namespace and open in - another is a `verify` notice (ruling 69), never a finding. `allowed_signers` is derived (never hand-edited) and held by + own namespace only (`identity revoke|rotate --everywhere` files one + change-set per namespace the holder has the key open in, to land in one + commit), and a key closed in one namespace and open in another is a + `verify` notice (ruling 69), never a finding. `allowed_signers` is derived (never hand-edited) and held by `verify`'s `[SIGNERS]` stage. Graph classes `A003`/`A005`; `A006` deferred. - **Signing (format 7 / spec v1.8, #70)** — `ledger-core/src/signing/`. A namespace's policy lists the required schemes; a signature is a sidecar diff --git a/ledger-cli/src/commands/authority.rs b/ledger-cli/src/commands/authority.rs index 4c638f18..831b709d 100644 --- a/ledger-cli/src/commands/authority.rs +++ b/ledger-cli/src/commands/authority.rs @@ -84,10 +84,10 @@ pub fn identity(root: Option, cmd: IdentityCmd) -> Result args.principal = principal.as_deref().map(str::parse).transpose()?; finish(author.identity_add(args)) } - IdentityCmd::Revoke { binding } => finish(author.identity_revoke(&binding.parse()?)), - IdentityCmd::Rotate { binding, key_file: path } => { + IdentityCmd::Revoke { binding, everywhere } => finish(author.identity_revoke(&binding.parse()?, everywhere)), + IdentityCmd::Rotate { binding, key_file: path, everywhere } => { let closes = binding.parse()?; - finish(author.identity_rotate(&closes, key_file(&path, String::new())?)) + finish(author.identity_rotate(&closes, key_file(&path, String::new())?, everywhere)) } IdentityCmd::Sync => Ok(EXIT_OK), } diff --git a/ledger-cli/src/commands/authority_enum.rs b/ledger-cli/src/commands/authority_enum.rs index 03a72088..746780ae 100644 --- a/ledger-cli/src/commands/authority_enum.rs +++ b/ledger-cli/src/commands/authority_enum.rs @@ -94,6 +94,10 @@ pub enum IdentityCmd { Revoke { /// `key:` — the binding to close binding: String, + /// Close the key in every namespace its principal holds it open in: + /// one change-set per namespace, to land in one commit (ruling 69) + #[arg(long)] + everywhere: bool, }, /// Close one of your bindings and bind a new key in the same act Rotate { @@ -101,6 +105,10 @@ pub enum IdentityCmd { binding: String, #[arg(long, value_name = "PATH")] key_file: std::path::PathBuf, + /// Rotate in every namespace you hold the closed key open in: one + /// change-set per namespace, to land in one commit (ruling 69) + #[arg(long)] + everywhere: bool, }, /// Rewrite each namespace's allowed_signers from the log (after a merge) Sync, diff --git a/ledger-cli/tests/key_across_namespaces.rs b/ledger-cli/tests/key_across_namespaces.rs index 5dfd0811..4705a4d4 100644 --- a/ledger-cli/tests/key_across_namespaces.rs +++ b/ledger-cli/tests/key_across_namespaces.rs @@ -5,8 +5,10 @@ //! untouched (AC-47, second bullet), and `verify` reports the split as a //! notice, never a finding. Within a namespace the rules of 2026-10-06 //! stand: a key is bound once there, a closed key is never bound again -//! there, and every check asks over all bindings of the key there. The -//! writer's close in every namespace it holds is issue 8's. +//! there, and every check asks over all bindings of the key there. A close +//! in every namespace the holder has the key open in is one invocation, +//! `--everywhere`: one change-set per namespace, each with the same `at`, +//! to land in one commit (LP-6.34; AC-47, third bullet). mod common; @@ -258,17 +260,91 @@ fn no_agent_identity_and_no_non_interactive_session_produces_a_key_close() { let before = repo.log_files(); for ns in [A, B] { let id = bindings(&repo, ns)[0].id.to_string(); - let piped = repo.piped(&["identity", "revoke", &id]); - assert_ne!(piped.status.code(), Some(0), "{}", common::both(&piped)); - for agent in ["claude@anthropic.com", "noreply@anthropic.com", "github-actions@github.com"] { - repo.act_as(agent); - let out = repo.tty(&["identity", "revoke", &id]); - assert_ne!(out.status.code(), Some(0), "{agent} in `{ns}`: {}", common::both(&out)); + for args in [vec!["identity", "revoke", &id], vec!["identity", "revoke", &id, "--everywhere"]] { + let piped = repo.piped(&args); + assert_ne!(piped.status.code(), Some(0), "{}", common::both(&piped)); + for agent in ["claude@anthropic.com", "noreply@anthropic.com", "github-actions@github.com"] { + repo.act_as(agent); + let out = repo.tty(&args); + assert_ne!(out.status.code(), Some(0), "{agent} in `{ns}`: {}", common::both(&out)); + } + repo.act_as(OWNER); } - repo.act_as(OWNER); } assert_eq!(repo.log_files(), before, "nothing filed"); let in_a = bindings(&repo, A)[0].id.to_string(); repo.ok(&["identity", "revoke", &in_a]); assert_eq!(repo.log_files().len(), before.len() + 1, "the holder, at a terminal, closes"); } + +/// The log files of `ns`, sorted. +fn log_files_in(repo: &Repo, ns: &str) -> Vec { + let mut out: Vec = std::fs::read_dir(repo.log_dir(ns)).map(|d| d.flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect()).unwrap_or_default(); + out.sort(); + out +} + +/// AC-47, third bullet: one invocation closes the key in every namespace +/// the holder has it open in — one change-set per namespace, the same +/// `at`, landing in one commit — and no split is left to notice. +#[test] +fn a_revoke_everywhere_files_one_change_set_per_namespace_with_one_at_in_one_commit() { + let (repo, _) = a_then_b(); + let (in_a, in_b) = (bindings(&repo, A)[0].clone(), bindings(&repo, B)[0].clone()); + let (before_a, before_b) = (log_files_in(&repo, A), log_files_in(&repo, B)); + let out = repo.ok(&["identity", "revoke", &in_a.id.to_string(), "--everywhere"]); + assert!(out.contains(&format!("closing {OWNER}'s key in 2 namespace(s): `{A}`, `{B}`")), "{out}"); + assert_eq!(out.matches("filed ").count(), 2, "one file per namespace named: {out}"); + assert_eq!(log_files_in(&repo, A).len(), before_a.len() + 1, "one change-set in A"); + assert_eq!(log_files_in(&repo, B).len(), before_b.len() + 1, "one change-set in B"); + let closes: Vec = ledger_core::store::load(repo.path()) + .log + .iter() + .flat_map(|l| l.file.key_bindings.iter()) + .filter(|b| b.closes.is_some()) + .cloned() + .collect(); + assert_eq!(closes.len(), 2, "{closes:?}"); + assert_eq!(closes[0].at, closes[1].at, "the same `at` in each namespace"); + assert!(closes.iter().any(|c| c.namespace == A && c.closes.as_ref() == Some(&in_a.id)), "A's close names A's binding"); + assert!(closes.iter().any(|c| c.namespace == B && c.closes.as_ref() == Some(&in_b.id)), "B's close names B's binding"); + hand::commit(&repo, "closed everywhere, one commit"); + let (code, text) = verify(&repo, &[]); + assert_eq!(code, 0, "{text}"); + assert!(!text.contains("is closed in"), "no split to notice: {text}"); + for ns in [A, B] { + assert!(repo.signers(ns).lines().filter(|l| !l.starts_with('#')).all(|l| l.contains("valid-before=")), "{ns}: {}", repo.signers(ns)); + } + // Closed everywhere, there is nothing left to close. + let again = repo.ledger(&["identity", "revoke", &in_b.id.to_string(), "--everywhere"]); + assert_eq!(again.status.code(), Some(1), "{}", common::both(&again)); + assert!(common::both(&again).contains("nothing to close"), "{}", common::both(&again)); +} + +#[test] +fn a_rotate_everywhere_closes_the_key_and_binds_the_new_one_in_each_namespace() { + let (repo, key) = a_then_b(); + let in_a = bindings(&repo, A)[0].id.to_string(); + let next = repo.keygen("owner-next"); + repo.use_key(&key); + let out = repo.ok(&["identity", "rotate", &in_a, "--key-file", &format!("{next}.pub"), "--everywhere"]); + assert!(out.contains("2 namespace(s)"), "{out}"); + let blob = std::fs::read_to_string(format!("{next}.pub")).expect("pub").split_whitespace().nth(1).map(str::to_string).expect("blob"); + for ns in [A, B] { + let here = bindings(&repo, ns); + let rotate = here.iter().find(|b| b.act == BindingAct::Rotate).expect("a rotate in each namespace"); + assert_eq!(rotate.key.as_deref(), Some(blob.as_str()), "{ns}: the new key"); + assert_eq!(rotate.closes.as_ref(), Some(&here[0].id), "{ns}: closes that namespace's binding"); + } + hand::commit(&repo, "rotated everywhere, one commit"); + let (code, text) = verify(&repo, &[]); + assert_eq!(code, 0, "{text}"); + assert!(!text.contains("is closed in"), "{text}"); + // The new key signs in B straight away; the old one does not. + let id = decision(&repo, B, "Signed with the rotated key."); + hand::commit(&repo, "a decision in B"); + let refused = refusal(&repo, &["accept", &id]); + assert!(refused.contains("it was closed in"), "{refused}"); + repo.use_key(&next); + repo.ok_tty(&["accept", &id]); +} diff --git a/ledger-core/src/author/identity_ops.rs b/ledger-core/src/author/identity_ops.rs index 722a16e7..5a718ab8 100644 --- a/ledger-core/src/author/identity_ops.rs +++ b/ledger-core/src/author/identity_ops.rs @@ -22,6 +22,7 @@ use super::sign_ops::ToSign; use super::{Applied, Author, AuthorError}; /// A public key, as OpenSSH writes it: type then base64. +#[derive(Clone)] pub struct KeyArgs { pub namespace: String, pub key_type: String, @@ -39,14 +40,69 @@ impl Author { } /// Close one of the actor's open bindings and open a new key in one act. - pub fn identity_rotate(&mut self, closes: &KeyBindingId, args: KeyArgs) -> Result { - self.bind(BindingAct::Rotate, Some(closes.clone()), Some(args)) + /// With `everywhere`, in every namespace the principal holds the closed + /// key open in (LP-6.34). + pub fn identity_rotate(&mut self, closes: &KeyBindingId, args: KeyArgs, everywhere: bool) -> Result { + match everywhere { + true => self.close_everywhere(BindingAct::Rotate, closes, Some(&args)), + false => self.bind(BindingAct::Rotate, Some(closes.clone()), Some(args)), + } } /// Close a binding's window — the principal's act, or the genesis /// holder's (a compromised key's owner may not be the one to notice). - pub fn identity_revoke(&mut self, closes: &KeyBindingId) -> Result { - self.bind(BindingAct::Revoke, Some(closes.clone()), None) + /// With `everywhere`, in every namespace the principal holds the key + /// open in (LP-6.34). + pub fn identity_revoke(&mut self, closes: &KeyBindingId, everywhere: bool) -> Result { + match everywhere { + true => self.close_everywhere(BindingAct::Revoke, closes, None), + false => self.bind(BindingAct::Revoke, Some(closes.clone()), None), + } + } + + /// A close in every namespace the key's principal holds it open in + /// (ruling 69, LP-6.34): one change-set per namespace, each the same + /// act with the same `at`, signed in its own namespace and gated as it + /// is filed, in the order of the namespaces' names. A close ends the key + /// in its own namespace only (LP-6.32), so this is how one invocation + /// ends a key everywhere; the files land in one commit. + fn close_everywhere(&mut self, act: BindingAct, closes: &KeyBindingId, key: Option<&KeyArgs>) -> Result { + let store = self.load(); + let all = Authority::build(&store); + let target = all + .bindings + .iter() + .copied() + .find(|b| b.id == *closes && b.act.opens()) + .ok_or_else(|| AuthorError::Usage(format!("{closes} opens no key window")))?; + let mut open: Vec<&KeyBinding> = all + .bindings + .iter() + .copied() + .filter(|o| o.act.opens() && o.principal == target.principal && o.key == target.key) + .filter(|o| !crate::authority::key_close::is_closed(&all.bindings, o)) + .collect(); + open.sort_by(|a, b| a.namespace.cmp(&b.namespace)); + if open.is_empty() { + return Err(AuthorError::Conflict(format!("{closes}'s key is closed in every namespace it was bound in — nothing to close"))); + } + let mut lines = vec![format!( + "closing {}'s key in {} namespace(s): {}", + target.principal, + open.len(), + open.iter().map(|o| format!("`{}`", o.namespace)).collect::>().join(", ") + )]; + let mut last = None; + for o in open { + let key = key.map(|k| KeyArgs { namespace: o.namespace.clone(), ..k.clone() }); + let applied = self.bind(act, Some(o.id.clone()), key)?; + lines.extend(applied.lines); + if let Some(previous) = last.replace(applied.path) { + lines.push(format!("filed {}", previous.display())); + } + } + let path = last.ok_or_else(|| AuthorError::Io("nothing filed".into()))?; + Ok(Applied { path, lines }) } fn bind( diff --git a/ledger/spec/ledger-protocol.md b/ledger/spec/ledger-protocol.md index 052ec503..e6057290 100644 --- a/ledger/spec/ledger-protocol.md +++ b/ledger/spec/ledger-protocol.md @@ -872,7 +872,8 @@ Until these rules, a policy by anyone with a bound key (or, under `[none]` or fo All of section 6.7 is **not implemented**. - **LP-6.31** (W, V) Authority is per namespace (spec v1.9, ruling 47). A grant, grant acceptance, unavailability, availability, revocation or role belongs to the namespace whose directory holds it (LP-3.33, LP-3.34); a binding and a policy carry their namespace. Each namespace has its own genesis grant, roles, grants and policy, and nothing in one namespace's authority has effect in another: every role check, genesis lookup and policy verdict reads one namespace's records (LP-6.16, LP-6.28), the authority shapes run over each namespace's graph alone (LP-8.19, LP-8.23), the unbound-genesis notice is per namespace (LP-8.31), and an export carries its own namespace's authority only (LP-9.11). A writer opens every namespace as it opens the first, on its own `external_ref` mandate, with its own root and accept roles and first policy (LP-4.38); a store whose namespaces shared one genesis is re-founded, not migrated (ruling 68, Appendix C). Keys are per namespace as well: the self-bound binding once per namespace (LP-4.12, LP-4.38), a first policy signed by a key trusted in its own namespace (LP-4.31), and keys judged within their namespace (LP-4.32, LP-4.37, LP-4.39, LP-6.32). -- **LP-6.32** (W, V) A close of a key takes effect in its own namespace only (spec v1.9, ruling 47; LP-4.39). A key closed in one namespace and open in another is a repository notice, never a finding (ruling 69, LP-8.31). A writer MAY file a close in every namespace it holds, one change-set per namespace in one commit (ruling 69). *The writer's close in every namespace held is not yet built (issue 8).* +- **LP-6.32** (W, V) A close of a key takes effect in its own namespace only (spec v1.9, ruling 47; LP-4.39). A key closed in one namespace and open in another is a repository notice, never a finding (ruling 69, LP-8.31). A writer MAY file a close in every namespace it holds, one change-set per namespace in one commit (ruling 69, LP-6.34). +- **LP-6.34** (W) **A close in every namespace held** (ruling 69). A writer that closes a key in every namespace its principal holds it open in files one change-set per namespace: each the same act, `rotate` or `revoke`, closing that namespace's binding of the key, with the same `at`, signed in its own namespace, to land in one commit. Without that instruction a close ends the key in the namespace of the binding it names alone (LP-6.32), and the split, if any, is a notice (LP-8.31). *Note: the reference writer's instruction is `ledger identity revoke|rotate --everywhere`; it files the closes namespace by namespace, in the order of the namespaces' names, each gated as it lands, and names every file it wrote.* Authority as its own unit, which namespaces depend on by pin, is intended for later and is not designed (ruling 48, section 12.3). @@ -1475,6 +1476,7 @@ A deployment should tell holders, before they accept a grant, that their address | 7 October 2026 | Ruling 58 applied: a landed `format:` declaration is compared across first-parent history, and any change other than the LP-3.16 correction fails `L007` (LP-3.16, LP-8.30, `L007`'s wording). This enforces the ruling of 5 October on #81; its three corrections stay green. No class added; no digest moves. | | 7 October 2026 | Ruling 59 applied: a `set:` grant scope accepts every valid set id, dots included (LP-3.3). The reference had refused a dot, which section 3.3 allows. No class added; no digest moves: a scope is hashed as written. | | 7 October 2026 | Ruling 60 applied: a change-set's `parents` is part of its header entity and immutable once landed (LP-8.25). The reference had keyed each parent as its own entity, so a parent appended to a landed header passed. No class added; no digest moves. | +| 9 October 2026 | **Ruling 69 applied: a close in every namespace held** (PRD §3.3; issue 8). LP-6.34 (new): a writer closes a key in every namespace its principal holds it open in with one change-set per namespace, the same act with the same `at`, to land in one commit; LP-6.32 loses its note. The writer's opening of every namespace on its own genesis (LP-4.38) landed with issues 5 and 6. No class added; no digest moves; no Appendix C note. | | 9 October 2026 | **Ruling 47 applied: `allowed_signers` per namespace** (PRD §3.4; issue 7). LP-4.10 loses its note: each namespace's file holds that namespace's trusted bindings, a line ended by a close in that namespace only (LP-4.32), a namespace that binds nothing has no file, and a writer that regenerates the files removes a stale one; LP-4.33 names the namespace a `[SIGNERS]` finding is about. No class added; no digest moves. The Appendix C note "A stale `allowed_signers` is removed" is added. | | 9 October 2026 | **Rulings 47 and 69 applied: keys per namespace** (PRD §3.3; issue 6). LP-6.32 loses its not-implemented mark: a close ends the key in its own namespace only; LP-4.37's three rules, LP-4.39 (with ruling 101's wording) and LP-4.13's deadline look at the binding's own namespace; the genesis holder's first key in a namespace is that namespace's self-bound binding, once per namespace, and a key trusted elsewhere vouches for nothing here (LP-4.12, LP-4.31, LP-4.38 — the later-namespace paragraph goes, with the reference's `carried_over`); `allowed_signers` ends a line at a close in its own namespace (LP-4.32); LP-8.31 gains the two notices of ruling 69. LP-6.31 drops its keys note. No class added; no digest moves. The Appendix C note "Keys are per namespace" is added. | | 9 October 2026 | **Ruling 47 applied: authority per namespace** (with rulings 67, 68 and 50 per namespace; PRD §3.2, §3.9; issue 5). LP-6.31 loses its not-implemented mark: a grant, grant acceptance, interval, revocation or role belongs to the namespace whose directory holds it; each namespace has its own genesis grant, roles and policy; a scope is read inside its own namespace, `ns:` being a schema fault (LP-6.16, LP-8.8); a policy is judged under its own namespace's genesis (LP-6.28); the authority shapes run over each namespace's graph alone, so `A003` and `A005` count per namespace, while the decision shapes keep the store's graph (LP-8.19, LP-8.23); roles are per namespace (LP-5.19); the unbound-genesis notice is per namespace (LP-8.31); an export carries its own authority only (LP-9.11). LP-6.5 loses its note. Keys stay store-wide until issue 6 (noted in LP-6.31). A writer opens every namespace on its own mandate. No class added; no digest moves; no grant payload gains a field. The Appendix C note "Authority is per namespace" is added. | From 5f23b62fb5a736a6238c303e0e81d7982322ee7e Mon Sep 17 00:00:00 2001 From: "Claude (Emil)" Date: Sun, 11 Oct 2026 06:20:35 +0000 Subject: [PATCH 2/2] Seam bindings for step 8: closing a key in every namespace held Files the signed bindings the contract-surface gate demands for this step's three exposed Rust surface changes, all in author/identity_ops.rs: the declaration is amended with the bindings and its verdict extended with what rotate and revoke do under `everywhere`. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GgRcHXmnAu43hChNxhnqxU --- ...edger-core-src-author-identity-ops-rs.yaml | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.ddd/seams/seam-rust-ledger-core-src-author-identity-ops-rs.yaml b/.ddd/seams/seam-rust-ledger-core-src-author-identity-ops-rs.yaml index 66c0990c..77f16c45 100644 --- a/.ddd/seams/seam-rust-ledger-core-src-author-identity-ops-rs.yaml +++ b/.ddd/seams/seam-rust-ledger-core-src-author-identity-ops-rs.yaml @@ -1,7 +1,7 @@ format: 2 id: seam/rust/ledger-core-src-author-identity-ops-rs boundary: contract surface of ledger-core/src/author/identity_ops.rs -verdict_knowledge: Callers learn identity add|rotate|revoke append key bindings (the namespace's first is the genesis holder's, self-bound under the mandate), refuse a software key under an -sk policy, and rewrite allowed_signers. +verdict_knowledge: 'Callers learn identity add|rotate|revoke append key bindings in one namespace (the namespace''s first is the genesis holder''s, self-bound under the mandate), refuse a software key under an -sk policy, rewrite that namespace''s allowed_signers, and that rotate and revoke with everywhere file one change-set per namespace the principal has the key open in, to land in one commit (LP-6.34).' contract_location: ledger-core/src/author/identity_ops.rs obligations: [] metadata: {} @@ -66,3 +66,21 @@ bindings: after: sha256:bead00f9272725f5dd6f5a1aed8e104e0952dc2de57a3c505f490c599b2f8b28 base_revision: 88b3de113c03fa70091d00fef217e55d44ccac3e hash: sha256:6a22798d9caf6c67109e21f5d717775fd87d1bf77439c166a0ca52a7265aecd9 +- symbol: KeyArgs + file: ledger-core/src/author/identity_ops.rs + before: sha256:8bea51520bb0b7ffe6eed9cda30c74dbc7edecba82e555148063f65a776f07e7 + after: sha256:65495d3f5ae13c3b9afe51dca8fb9aca2c43fefa1ced4f3740169de3d61087c2 + base_revision: 111f4f611f1e31b07843a4211292f0c6d7d3e500 + hash: sha256:1d17d847b3c0af6a505e4ae9b5444bf3e8ba2646a01d20201cd1da687bb88939 +- symbol: identity_revoke + file: ledger-core/src/author/identity_ops.rs + before: sha256:8bea51520bb0b7ffe6eed9cda30c74dbc7edecba82e555148063f65a776f07e7 + after: sha256:65495d3f5ae13c3b9afe51dca8fb9aca2c43fefa1ced4f3740169de3d61087c2 + base_revision: 111f4f611f1e31b07843a4211292f0c6d7d3e500 + hash: sha256:c3c6135fa7c45bd808835e06a1b272454571dc6f5eb8e5096153f46565a16b53 +- symbol: identity_rotate + file: ledger-core/src/author/identity_ops.rs + before: sha256:8bea51520bb0b7ffe6eed9cda30c74dbc7edecba82e555148063f65a776f07e7 + after: sha256:65495d3f5ae13c3b9afe51dca8fb9aca2c43fefa1ced4f3740169de3d61087c2 + base_revision: 111f4f611f1e31b07843a4211292f0c6d7d3e500 + hash: sha256:85346982663c44e63ae8712fd40804749d996059e1e76b45cbc61ccf5927b608