From dd134feed93f7928e445a9ac09876c0eba9bae1d Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 11:43:19 -0400 Subject: [PATCH 1/7] =?UTF-8?q?feat(recovery):=20Stage=204=20=E2=80=94=20r?= =?UTF-8?q?ecovery=20schema,=20shared=20controller,=20compiler=20+=20clien?= =?UTF-8?q?t=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalizes Stage 4 of the perch recovery plan (follow-up.md), generalizing the validated Nido Stage 3 experiment (fm/nido-recovery-stage3-n8, PR nidohq/nido#206) into perch itself: - perch-ir: `recovery: Option` on PolicyDoc (guardian-only / zk-only / combined modes, Loss/Protected profiles, non-circular baseline commitment, no-default pending-activity policy per §7), canonicalized per CANONICAL.md's existing omit-when-None convention, and validated. New ci-publish-recovery{,-combined} conformance fixtures extend testdata/ in lockstep Rust+TS; existing fixtures are byte-for-byte unchanged (regression tested). - perch-doc-compiler: lowers the doc's recovery section into `CompiledRecoveryConfig` (a new field on `CompiledDoc`), resolving replaceable signer ids to credential fingerprints so revocation survives id reuse across documents. - perch-recovery (new crate): the shared recovery controller as an OZ `Policy` — initiation/delay/expiry/cancel/completion, guardian quorum and a generic ZK verifier adapter (no circuit shipped — see docs/recovery/controller-governance.md), config_hash, Variant A completion (recovery authorizes apply_doc via a zero-signer `"recovery"` context rule), and a general Protected-reconfigure gate (`guard_apply_doc`) generalizing Nido's additive-only reconfigure to the full requirement. `contract`-feature-gated like perch-doc-compiler so consumers link only the client. - perch-smart-account: `apply_doc` grows a `recovery_evidence` parameter, installs/removes the recovery rule, and gates every call through `guard_apply_doc` whenever a controller is enrolled. - perch-js: schema/builder support for `recovery`, parity tests proving the builder reproduces the Rust-pinned hashes. - docs/recovery/: schema design, controller governance, VK/controller immutability review, account-mutation-path inventory, migration doc for existing non-upgradeable accounts, Lean/conformance impact assessment, and the §7 pending-activity gate recorded as an explicit, release-blocking, unresolved parameter (not decided by this change). - .github/workflows/release.yml: registers perch-recovery for tagging/version-pin tracking, deliberately excluded from the constructorless publish allow-list until on-chain publish is a separate, deliberate decision. End-to-end guardian-only recovery (enroll → initiate → guardian quorum → timelock → complete, replay/expiry/wrong-target refusal, cancellation as a separate evidence domain, Protected-reconfigure gating) is proven against the real OZ `do_check_auth`/`Policy::enforce` path in crates/integration-tests/tests/recovery.rs. --- .github/workflows/release.yml | 16 +- AGENTS.md | 69 + Cargo.lock | 15 + Cargo.toml | 1 + README.md | 5 + crates/integration-tests/Cargo.toml | 1 + ...lls_rules_and_stores_canonical_hash.1.json | 34 +- ...alls_the_cap_beside_the_interpreter.1.json | 128 +- ...s_a_cap_token_that_is_not_the_scope.1.json | 6 +- .../completion_cannot_be_replayed.1.json | 1102 ++++++++++++++++ ...refused_before_the_timelock_elapses.1.json | 1070 +++++++++++++++ ...etion_refused_below_guardian_quorum.1.json | 1025 +++++++++++++++ ...fused_for_the_wrong_target_document.1.json | 1070 +++++++++++++++ ...ion_refused_with_no_evidence_at_all.1.json | 978 ++++++++++++++ ...doc_for_another_network_is_rejected.1.json | 4 +- ...t_admin_rule_is_rejected_anti_brick.1.json | 6 +- ...age_and_unknown_fields_are_rejected.1.json | 6 +- ...ate_domain_from_initiation_approval.1.json | 1160 +++++++++++++++++ ...es_and_installs_the_target_document.1.json | 1104 ++++++++++++++++ ...ever_promotes_without_being_reached.1.json | 1107 ++++++++++++++++ ..._mutation_entry_points_do_not_exist.1.json | 6 +- ...ian_evidence_admin_alone_is_refused.1.json | 811 ++++++++++++ ...reapply_replaces_the_whole_rule_set.1.json | 62 +- crates/integration-tests/tests/apply_doc.rs | 29 +- .../integration-tests/tests/apply_doc_cap.rs | 9 +- crates/integration-tests/tests/cap_matrix.rs | 1 + crates/integration-tests/tests/recovery.rs | 359 +++++ .../perch-conformance/tests/differential.rs | 1 + crates/perch-doc-compiler/src/lib.rs | 232 +++- crates/perch-ir/src/canon.rs | 83 +- crates/perch-ir/src/doc.rs | 168 +++ crates/perch-ir/src/lib.rs | 8 +- crates/perch-ir/src/parse.rs | 158 ++- crates/perch-ir/src/validate.rs | 241 +++- crates/perch-ir/tests/common/mod.rs | 1 + crates/perch-ir/tests/recovery.rs | 332 +++++ crates/perch-recovery/Cargo.toml | 36 + crates/perch-recovery/src/contract.rs | 641 +++++++++ crates/perch-recovery/src/lib.rs | 116 ++ crates/perch-recovery/src/storage.rs | 38 + crates/perch-recovery/src/types.rs | 84 ++ crates/perch-recovery/src/zk.rs | 89 ++ crates/perch-smart-account/Cargo.toml | 4 + crates/perch-smart-account/src/lib.rs | 64 +- crates/perch-testkit/Cargo.toml | 1 + crates/perch-testkit/src/fixture.rs | 12 + crates/perch-testkit/src/lib.rs | 7 +- docs/recovery/README.md | 36 + docs/recovery/account-mutation-paths.md | 64 + docs/recovery/controller-governance.md | 215 +++ docs/recovery/formal-verification-impact.md | 90 ++ docs/recovery/migration.md | 135 ++ docs/recovery/schema.md | 173 +++ docs/recovery/section-7-gate.md | 81 ++ .../vk-and-controller-immutability.md | 116 ++ packages/perch-js/src/builder.ts | 81 ++ packages/perch-js/src/schema.ts | 41 + packages/perch-js/test/builder.test.ts | 77 ++ packages/perch-js/test/parity.test.ts | 45 + packages/perch-js/test/schema.test.ts | 86 ++ testdata/README.md | 5 +- ...i-publish-recovery-combined.canonical.json | 1 + .../ci-publish-recovery-combined.doc-hash | 1 + testdata/ci-publish-recovery-combined.json | 56 + testdata/ci-publish-recovery.canonical.json | 1 + testdata/ci-publish-recovery.doc-hash | 1 + testdata/ci-publish-recovery.json | 57 + 67 files changed, 13764 insertions(+), 98 deletions(-) create mode 100644 crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json create mode 100644 crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json create mode 100644 crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json create mode 100644 crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json create mode 100644 crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json create mode 100644 crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json create mode 100644 crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json create mode 100644 crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json create mode 100644 crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json create mode 100644 crates/integration-tests/tests/recovery.rs create mode 100644 crates/perch-ir/tests/recovery.rs create mode 100644 crates/perch-recovery/Cargo.toml create mode 100644 crates/perch-recovery/src/contract.rs create mode 100644 crates/perch-recovery/src/lib.rs create mode 100644 crates/perch-recovery/src/storage.rs create mode 100644 crates/perch-recovery/src/types.rs create mode 100644 crates/perch-recovery/src/zk.rs create mode 100644 docs/recovery/README.md create mode 100644 docs/recovery/account-mutation-paths.md create mode 100644 docs/recovery/controller-governance.md create mode 100644 docs/recovery/formal-verification-impact.md create mode 100644 docs/recovery/migration.md create mode 100644 docs/recovery/schema.md create mode 100644 docs/recovery/section-7-gate.md create mode 100644 docs/recovery/vk-and-controller-immutability.md create mode 100644 testdata/ci-publish-recovery-combined.canonical.json create mode 100644 testdata/ci-publish-recovery-combined.doc-hash create mode 100644 testdata/ci-publish-recovery-combined.json create mode 100644 testdata/ci-publish-recovery.canonical.json create mode 100644 testdata/ci-publish-recovery.doc-hash create mode 100644 testdata/ci-publish-recovery.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1c29dc..a50aeac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: # crates//Cargo.toml in a PR; on merge this tags + publishes # it. The constructorless allow-list in publish-plan decides which of # these actually publish on-chain (the rest just get a tag). - CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry" + CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry perch-recovery" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" releases='[]' @@ -240,10 +240,14 @@ jobs: paths_for() { case "$1" in perch-interpreter) echo "perch-interpreter perch-program";; perch-doc-compiler) echo "perch-doc-compiler perch-compile perch-ir perch-program";; - perch-account) echo "perch-account perch-smart-account perch-doc-compiler perch-compile perch-ir perch-program perch-registry-resolve perch-registry-resolve-macro";; + # perch-smart-account depends on perch-recovery (the RecoveryController + # client apply_doc cross-calls), so a perch-recovery change re-releases + # the account too — see AGENTS.md's version-pin sharp edge. + perch-account) echo "perch-account perch-smart-account perch-doc-compiler perch-compile perch-ir perch-program perch-registry-resolve perch-registry-resolve-macro perch-recovery";; + perch-recovery) echo "perch-recovery perch-doc-compiler perch-compile perch-ir perch-program";; *) echo "$1";; esac; } - CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry" + CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry perch-recovery" summary="" for c in $CONTRACTS; do inc=() @@ -618,6 +622,12 @@ jobs: # The constructorless set. Keep in step with the detect-releases # CONTRACTS list. A contract not listed here is tagged but never # published to the constructorless registry. + # perch-recovery is deliberately NOT in this allow-list yet: it is + # tagged/versioned (CONTRACTS above) so its intra-workspace pins stay + # tracked, but publishing a recovery controller on-chain is a + # separate, deliberate decision (see docs/recovery/section-7-gate.md) + # — add it here explicitly when that decision is made, not as a side + # effect of adding the crate. ALLOW='["perch-interpreter","perch-doc-compiler","perch-ed25519-verifier"]' if [ -n "$DISPATCH_TAG" ]; then # Strict allow-list regex, not a prefix glob: the tag and the package diff --git a/AGENTS.md b/AGENTS.md index 63cbdcd..8da26dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,75 @@ fix — it only tags versions with no existing tag. Recovering a build-that-never-published requires a fresh version bump (see PR #79), not a rerun once the underlying commit's tree is fixed. +Adding a brand-new deployable contract crate needs entries in **four** +places in `release.yml`, not just workspace membership: both `CONTRACTS=` +lists (`detect-releases` and `release-pr` — kept as duplicated literals, not +one shared value), a `paths_for()` case for its own intra-workspace scope, +and — only once the contract is actually meant to auto-publish on-chain when +tagged — the `publish-plan` job's `ALLOW` list and `DISPATCH_TAG` regex. +Deliberately leaving a new contract out of `ALLOW` (while still tagging it +via `CONTRACTS`/`paths_for()`) is the correct way to ship its source and keep +its version-bump/pin tracking correct without triggering an on-chain publish +before that's a deliberate decision — see `perch-recovery`'s introduction +(PR that added `docs/recovery/`) for a worked example of this split. + +## Infra contracts are resolved from a build-time-pinned wasm hash, not a runtime address + +`perch-smart-account`'s `infra` module (`perch_registry_resolve::registry_contract!`) +derives the doc-compiler/interpreter/spending-limit addresses as +`deployer(stateless_registry_id, sha256(pinned_wasm_bytes))`, where the wasm +bytes are fetched into the build tree (`scripts/fetch-infra-wasm.sh`) and +baked into the *consuming* contract's own wasm at compile time — there is no +constructor argument or storage slot naming these addresses, and no way to +repoint them post-deploy. Two consequences that are easy to miss: + +- **Any change to the doc/wire schema the doc-compiler accepts or emits + (`perch-ir`, `CompiledDoc`/`CompiledRule`) requires a new `perch-doc-compiler` + build, which is a new address — no existing deployed `PerchAccount` will + ever call it, no matter what's published later.** A schema change is never + a live upgrade for deployed accounts; it's a new option only newly-built + accounts can use. See `docs/recovery/migration.md` for the worked-out + consequence (an account built before a schema change can never adopt it in + place — only a new account can). +- A new deployable that other in-repo crates need to *cross-call the client + of* without linking its full logic (parser, storage, `Policy` impl, ...) + should split like `perch-doc-compiler` does: a `contract` feature + (default-on) gating the actual `#[contract]` struct/impl, plus an + always-available hand-written `#[contractclient]` trait outside that gate + for the entry points consumers actually call. `perch-recovery` follows this + split (`RecoveryControllerClient`, ungated, vs. the full `PerchRecovery` + contract, gated) so `perch-smart-account` links no controller storage or + `Policy`-lifecycle code into account wasm. + +## Soroban auth mocking never invokes a custom account's `__check_auth` + +`env.mock_all_auths()`/`mock_all_auths_allowing_non_root_auth()` puts the +host in recording-auth mode, which — confirmed by reading +`soroban-env-host`'s own source (`auth.rs`, `require_auth_recording`: "we +don't call `__check_auth` in this flow") — **skips invoking a custom +account's `__check_auth` entirely**, for every address including the account +under test. `perch_testkit::Bootstrap`'s `World` uses this mode, so any test +built on it (the whole `apply_doc*.rs`/`cap_matrix.rs` suite) proves +*compiler-level* validation and rule-installation shape, never OZ's +context-rule *selection*/`Policy::enforce` mechanics — those calls succeed +regardless of which rule, if any, would actually have authorized them. +To test real rule-selection/policy-enforcement behavior (as `matrix.rs` does, +and `crates/integration-tests/tests/recovery.rs` following its pattern for +the recovery controller), call +`stellar_accounts::smart_account::do_check_auth` directly with a hand-built +`AuthPayload`/`Context`, wrapped in `env.as_contract(&account, || {...})` — +this bypasses host-level auth entirely rather than depending on it, so it +works the same with or without mocking. Relatedly: OZ's +`remove_context_rule` calls a policy's `uninstall` via `try_uninstall` and +discards the result even if it panics — a policy can never rely on +`uninstall` as a security gate (see `docs/recovery/controller-governance.md` +for how `perch-recovery` handles this). When chaining several +`do_check_auth` calls that are expected to panic (via +`catch_unwind`) inside one test, keep it to one or two per `Env` — this +host's test call-stack bookkeeping was observed not to reliably survive a +longer chain of recovered panics in one `Env`; prefer a fresh `Env` (a new +`setup()`) per scenario instead of accumulating them. + ## Maintaining this file Keep this file for knowledge useful to almost every future agent session in this project. diff --git a/Cargo.lock b/Cargo.lock index 785f18d..746123f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1509,6 +1509,7 @@ dependencies = [ "perch-interpreter", "perch-ir", "perch-program", + "perch-recovery", "perch-registry-resolve", "perch-smart-account", "perch-spending-limit", @@ -1543,6 +1544,18 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "perch-recovery" +version = "0.1.0" +dependencies = [ + "perch-doc-compiler", + "perch-ir", + "perch-testkit", + "soroban-sdk", + "soroban-sdk-tools", + "stellar-accounts", +] + [[package]] name = "perch-registry-resolve" version = "0.1.1" @@ -1566,6 +1579,7 @@ name = "perch-smart-account" version = "0.1.1" dependencies = [ "perch-doc-compiler", + "perch-recovery", "perch-registry-resolve", "soroban-sdk", "soroban-sdk-tools", @@ -1598,6 +1612,7 @@ dependencies = [ "perch-doc-compiler", "perch-ed25519-verifier", "perch-interpreter", + "perch-recovery", "perch-smart-account", "perch-spending-limit", "soroban-sdk", diff --git a/Cargo.toml b/Cargo.toml index 9507443..7405f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/perch-registry-resolve-macro", "crates/perch-registry-resolve", "crates/perch-testkit", + "crates/perch-recovery", ] [workspace.package] diff --git a/README.md b/README.md index e76e52b..e05b160 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ crates/ perch-smart-account/ the doc-only account trait: apply_doc (the sole write path) on OZ perch-account/ deployable shell of perch-smart-account (6 exported functions, ~28 KB) perch-ed25519-verifier/ deployable ed25519 verifier for External signers + perch-recovery/ deployable account-recovery controller (guardian/ZK/combined modes), + an OZ Policy attached via apply_doc's `recovery` document field — + see docs/recovery/ perch-deploy/ deploy/CI bin: signs smart-account auth entries (apply_doc, publish) perch-conformance/ eval-semantics conformance vectors: hand-authored (program, invocation) → verdict cases + compile→eval differential + wasm-leg suites @@ -60,6 +63,8 @@ komet/ Komet (K-framework) symbolic property tests — an indepen scripts/ bootstrap-testnet.sh — one-time registry + account bootstrap docs/slides/ the perch story as an HTML deck (served via GitHub Pages) docs/verification/ the layered verification plan (PLAN.md) + enforceability theory (THEORY.md) +docs/recovery/ opt-in account recovery: schema, controller governance, migration, + and the open release-blocking pending-activity gate docs/testnet-deployment.md verified live-state map of the canonical testnet deployment testdata/ golden vectors shared by the Rust and TS suites (frozen) testdata/eval/ eval-semantics vectors shared by Rust, the Lean model, and the wasm leg diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index add9bb5..23f138e 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -22,6 +22,7 @@ perch-ed25519-verifier = { version = "0.1.1", path = "../perch-ed25519-verifier" perch-spending-limit = { version = "0.1.1", path = "../perch-spending-limit" } perch-testkit = { version = "0.1.1", path = "../perch-testkit" } perch-registry-resolve = { version = "0.1.1", path = "../perch-registry-resolve" } +perch-recovery = { version = "0.1.0", path = "../perch-recovery" } stellar-accounts = { workspace = true } soroban-sdk = { workspace = true, features = ["testutils"] } ed25519-dalek = "2" diff --git a/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json b/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json index 254d288..de18a82 100644 --- a/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json +++ b/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json @@ -23,6 +23,34 @@ "args": [ { "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b0a202020202020226964223a202261646d696e222c0a202020202020227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c0a202020202020226b6579223a202230343565326137353839623733633139643533343163663132616330633566366334356332393864346332303030326337393464616164616664623833663335663562653233393633363438643761616363663565323733383033663266656337613866306562346434383435633962383961393732623461303932393862313765220a202020207d2c0a202020207b0a202020202020226964223a20226369222c0a202020202020227665726966696572223a2022434359574c4e575259444341454d324132454d5457414d49475745535147554a4e4454525246494f5335434250524f3534455a3237414247222c0a202020202020226b6579223a202231636536303430623064303332333261633663393131623063333735663161353265626465666666353666643336316431333638306532336361353738613137220a202020207d0a20205d2c0a20202272756c6573223a205b0a202020207b0a202020202020226e616d65223a202261646d696e222c0a2020202020202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d0a202020207d2c0a202020207b0a202020202020226e616d65223a202263692d7075626c697368222c0a2020202020202273636f7065223a207b0a20202020202020202274797065223a2022636f6e7472616374222c0a20202020202020202261646472657373223a202243434137514141364f44364c514a5455324d4b4e36454153354935325149465041594d4d51595355374b48575447543236414e364e32414c220a2020202020207d2c0a202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b226369225d207d2c0a2020202020202266756e6374696f6e73223a205b227075626c697368222c20227075626c6973685f68617368225d2c0a2020202020202261726773223a205b0a20202020202020207b2022696e646578223a20312c202270726564223a207b202274797065223a202269732d73656c6622207d207d0a2020202020205d2c0a202020202020226e6f742d61667465722d6c6564676572223a2035353030303030300a202020207d0a20205d0a7d0a" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] } ] } @@ -703,7 +731,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -726,7 +754,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -749,7 +777,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json b/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json index a6fe3e6..931a572 100644 --- a/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json +++ b/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json @@ -23,6 +23,34 @@ "args": [ { "bytes": "7b0a202020202020202020202276657273696f6e223a20312c0a20202020202020202020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a20202020202020202020227369676e657273223a205b0a2020202020202020202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d2c0a2020202020202020202020207b20226964223a20226369222c20227665726966696572223a2022434359574c4e575259444341454d324132454d5457414d49475745535147554a4e4454525246494f5335434250524f3534455a3237414247222c20226b6579223a20223163653630343062306430333233326163366339313162306333373566316135326562646566666635366664333631643133363830653233636135373861313722207d0a202020202020202020205d2c0a202020202020202020202272756c6573223a205b0a2020202020202020202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a202020202020202020202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d2c0a2020202020202020202020207b20226e616d65223a2022636170706564222c202273636f7065223a207b202274797065223a2022636f6e7472616374222c202261646472657373223a202243434137514141364f44364c514a5455324d4b4e36454153354935325149465041594d4d51595355374b48575447543236414e364e32414c22207d2c0a202020202020202020202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b226369225d207d2c0a2020202020202020202020202020202266756e6374696f6e73223a205b227472616e73666572225d2c0a20202020202020202020202020202022636170223a207b20226c696d6974223a20223130222c2022706572696f642d6c656467657273223a2031303030207d207d0a202020202020202020205d0a20202020202020207d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] } ] } @@ -307,7 +335,7 @@ "symbol": "policy" }, "val": { - "address": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU" + "address": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV" } } ] @@ -358,7 +386,7 @@ "symbol": "PolicyLookup" }, { - "address": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU" + "address": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV" } ] }, @@ -762,7 +790,53 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": { "vec": [ { @@ -825,53 +899,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": null - } - } - } - }, - "ext": "v0" - }, - "live_until": 4095 - }, - { - "entry": { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": null - } - } - } - }, - "ext": "v0" - }, - "live_until": 4095 - }, - { - "entry": { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/apply_doc_rejects_a_cap_token_that_is_not_the_scope.1.json b/crates/integration-tests/test_snapshots/apply_doc_rejects_a_cap_token_that_is_not_the_scope.1.json index 53dd546..507afe8 100644 --- a/crates/integration-tests/test_snapshots/apply_doc_rejects_a_cap_token_that_is_not_the_scope.1.json +++ b/crates/integration-tests/test_snapshots/apply_doc_rejects_a_cap_token_that_is_not_the_scope.1.json @@ -295,7 +295,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -318,7 +318,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -341,7 +341,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json new file mode 100644 index 0000000..d88c074 --- /dev/null +++ b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json @@ -0,0 +1,1102 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Completed" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Revoked" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json new file mode 100644 index 0000000..6772621 --- /dev/null +++ b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json @@ -0,0 +1,1070 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "AuthorizedPending" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json new file mode 100644 index 0000000..715f76c --- /dev/null +++ b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json @@ -0,0 +1,1025 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json new file mode 100644 index 0000000..73256d8 --- /dev/null +++ b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json @@ -0,0 +1,1070 @@ +{ + "generators": { + "address": 6, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "AuthorizedPending" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json new file mode 100644 index 0000000..160e61f --- /dev/null +++ b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json @@ -0,0 +1,978 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/doc_for_another_network_is_rejected.1.json b/crates/integration-tests/test_snapshots/doc_for_another_network_is_rejected.1.json index e3ad2e3..d0e4333 100644 --- a/crates/integration-tests/test_snapshots/doc_for_another_network_is_rejected.1.json +++ b/crates/integration-tests/test_snapshots/doc_for_another_network_is_rejected.1.json @@ -344,7 +344,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCJQPFITUT4GIIXFBQHHPK5M25OX5BNU4P3KQHHJTRZM6JXXBITHPYLX", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -413,7 +413,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CDYGIFKT22KR4UF3WFEKRTX7LJIMWQOGYM3SPLROFFYNDF7JWC55HIY2", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/doc_without_admin_rule_is_rejected_anti_brick.1.json b/crates/integration-tests/test_snapshots/doc_without_admin_rule_is_rejected_anti_brick.1.json index fd3b3f3..5304221 100644 --- a/crates/integration-tests/test_snapshots/doc_without_admin_rule_is_rejected_anti_brick.1.json +++ b/crates/integration-tests/test_snapshots/doc_without_admin_rule_is_rejected_anti_brick.1.json @@ -296,7 +296,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -319,7 +319,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -342,7 +342,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/garbage_and_unknown_fields_are_rejected.1.json b/crates/integration-tests/test_snapshots/garbage_and_unknown_fields_are_rejected.1.json index 53dd546..507afe8 100644 --- a/crates/integration-tests/test_snapshots/garbage_and_unknown_fields_are_rejected.1.json +++ b/crates/integration-tests/test_snapshots/garbage_and_unknown_fields_are_rejected.1.json @@ -295,7 +295,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -318,7 +318,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -341,7 +341,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json new file mode 100644 index 0000000..0aaccaa --- /dev/null +++ b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json @@ -0,0 +1,1160 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "1" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelTally" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelsUsed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "2" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "4837995959683129791" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json new file mode 100644 index 0000000..a8b0b3f --- /dev/null +++ b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json @@ -0,0 +1,1104 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Completed" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Revoked" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json new file mode 100644 index 0000000..4e24269 --- /dev/null +++ b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json @@ -0,0 +1,1107 @@ +{ + "generators": { + "address": 6, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414d445234225d2c202271756f72756d223a2033207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "6b545f185dfe590ee4744294b081d9c00cfaeb3dd6259dfe96be6cca3fbc5132" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "AuthorizedPending" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "e90019b4b8403de3c8074e541ebf56822dc524556b24804704faf7e5f8e75616" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 3 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": "4837995959683129791" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/piecemeal_mutation_entry_points_do_not_exist.1.json b/crates/integration-tests/test_snapshots/piecemeal_mutation_entry_points_do_not_exist.1.json index a9be5c7..a2d7c12 100644 --- a/crates/integration-tests/test_snapshots/piecemeal_mutation_entry_points_do_not_exist.1.json +++ b/crates/integration-tests/test_snapshots/piecemeal_mutation_entry_points_do_not_exist.1.json @@ -301,7 +301,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -324,7 +324,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -347,7 +347,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json new file mode 100644 index 0000000..74144dd --- /dev/null +++ b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json @@ -0,0 +1,811 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a202270726f746563746564222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "55e8135647be7c129d294677b9882f5d453692575f140cb218d36aa975816fb8" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Protected" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json b/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json index 75a85b6..6932884 100644 --- a/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json +++ b/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json @@ -23,6 +23,34 @@ "args": [ { "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b0a202020202020226964223a202261646d696e222c0a202020202020227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c0a202020202020226b6579223a202230343565326137353839623733633139643533343163663132616330633566366334356332393864346332303030326337393464616164616664623833663335663562653233393633363438643761616363663565323733383033663266656337613866306562346434383435633962383961393732623461303932393862313765220a202020207d2c0a202020207b0a202020202020226964223a20226369222c0a202020202020227665726966696572223a2022434359574c4e575259444341454d324132454d5457414d49475745535147554a4e4454525246494f5335434250524f3534455a3237414247222c0a202020202020226b6579223a202231636536303430623064303332333261633663393131623063333735663161353265626465666666353666643336316431333638306532336361353738613137220a202020207d0a20205d2c0a20202272756c6573223a205b0a202020207b0a202020202020226e616d65223a202261646d696e222c0a2020202020202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d0a202020207d2c0a202020207b0a202020202020226e616d65223a202263692d7075626c697368222c0a2020202020202273636f7065223a207b0a20202020202020202274797065223a2022636f6e7472616374222c0a20202020202020202261646472657373223a202243434137514141364f44364c514a5455324d4b4e36454153354935325149465041594d4d51595355374b48575447543236414e364e32414c220a2020202020207d2c0a202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b226369225d207d2c0a2020202020202266756e6374696f6e73223a205b227075626c697368222c20227075626c6973685f68617368225d2c0a2020202020202261726773223a205b0a20202020202020207b2022696e646578223a20312c202270726564223a207b202274797065223a202269732d73656c6622207d207d0a2020202020205d2c0a202020202020226e6f742d61667465722d6c6564676572223a2035353030303030300a202020207d0a20205d0a7d0a" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] } ] } @@ -42,6 +70,34 @@ "args": [ { "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a2022616122207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c0a2020202020202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] } ] } @@ -396,7 +452,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCPBUYP4VEZAFQP2FKOXMKA2IDUCC2GGDFFZQ2SDHQVYXU75IJ6HLLXU", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -419,7 +475,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -442,7 +498,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { diff --git a/crates/integration-tests/tests/apply_doc.rs b/crates/integration-tests/tests/apply_doc.rs index b344576..df78b64 100644 --- a/crates/integration-tests/tests/apply_doc.rs +++ b/crates/integration-tests/tests/apply_doc.rs @@ -7,7 +7,7 @@ //! `perch_testkit::Bootstrap::native()` call; the stand-in verifier, the //! fixture constants and `fixture()` live in the testkit. -use perch_testkit::{fixture, Bootstrap, World, FIXTURE_NETWORK}; +use perch_testkit::{fixture, no_recovery_evidence, Bootstrap, World, FIXTURE_NETWORK}; use soroban_sdk::testutils::Ledger; use soroban_sdk::{vec, Bytes, BytesN, String as SString, Symbol, Val}; @@ -20,7 +20,8 @@ fn setup() -> World { fn apply_fixture(w: &World) -> BytesN<32> { let doc = Bytes::from_slice(&w.env, fixture().as_bytes()); - w.account_client().apply_doc(&doc) + w.account_client() + .apply_doc(&doc, &no_recovery_evidence(&w.env)) } #[test] @@ -71,7 +72,10 @@ fn reapply_replaces_the_whole_rule_set() { ] }}"# ); - let second = client.apply_doc(&Bytes::from_slice(&w.env, doc2.as_bytes())); + let second = client.apply_doc( + &Bytes::from_slice(&w.env, doc2.as_bytes()), + &no_recovery_evidence(&w.env), + ); assert_ne!(first, second); assert_eq!(client.applied_doc_hash(), Some(second)); @@ -132,7 +136,10 @@ fn doc_without_admin_rule_is_rejected_anti_brick() { }}"# ); assert!(client - .try_apply_doc(&Bytes::from_slice(&w.env, doc.as_bytes())) + .try_apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env) + ) .is_err()); // Nothing changed: the constructor's rule 0 is still the only rule. assert_eq!(client.get_context_rules_count(), 1); @@ -159,7 +166,9 @@ fn doc_for_another_network_is_rejected() { let client = w.account_client(); let doc = Bytes::from_slice(&w.env, fixture().as_bytes()); - assert!(client.try_apply_doc(&doc).is_err()); + assert!(client + .try_apply_doc(&doc, &no_recovery_evidence(&w.env)) + .is_err()); assert_eq!(client.applied_doc_hash(), None); } @@ -169,11 +178,17 @@ fn garbage_and_unknown_fields_are_rejected() { let client = w.account_client(); // Not JSON at all. assert!(client - .try_apply_doc(&Bytes::from_slice(&w.env, b"not json")) + .try_apply_doc( + &Bytes::from_slice(&w.env, b"not json"), + &no_recovery_evidence(&w.env) + ) .is_err()); // Unknown field: fail closed, never skipped. let doc = fixture().replace("\"version\": 1,", "\"version\": 1, \"surprise\": true,"); assert!(client - .try_apply_doc(&Bytes::from_slice(&w.env, doc.as_bytes())) + .try_apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env) + ) .is_err()); } diff --git a/crates/integration-tests/tests/apply_doc_cap.rs b/crates/integration-tests/tests/apply_doc_cap.rs index be1c09a..b389437 100644 --- a/crates/integration-tests/tests/apply_doc_cap.rs +++ b/crates/integration-tests/tests/apply_doc_cap.rs @@ -6,7 +6,7 @@ //! `perch-spending-limit`'s own test and `cap_matrix`. use perch_smart_account::infra; -use perch_testkit::{Bootstrap, World, FIXTURE_NETWORK}; +use perch_testkit::{no_recovery_evidence, Bootstrap, World, FIXTURE_NETWORK}; use soroban_sdk::{Address, Bytes, BytesN, String as SString}; /// admin verifier / ci verifier the testkit stands up in native mode. @@ -60,7 +60,7 @@ fn apply_doc_installs_the_cap_beside_the_interpreter() { // Previously `CapUnsupported`; now it compiles and applies. let doc = Bytes::from_slice(&w.env, capped_doc("").as_bytes()); - let hash: BytesN<32> = client.apply_doc(&doc); + let hash: BytesN<32> = client.apply_doc(&doc, &no_recovery_evidence(&w.env)); assert_eq!(client.applied_doc_hash(), Some(hash)); assert_eq!(client.get_context_rules_count(), 2); @@ -86,7 +86,10 @@ fn apply_doc_rejects_a_cap_token_that_is_not_the_scope() { // validation (it would silently meter a different contract). let mismatched = format!(r#""token": "{CI_VERIFIER}", "#); // any C-addr != TOKEN let doc = Bytes::from_slice(&w.env, capped_doc(&mismatched).as_bytes()); - assert!(w.account_client().try_apply_doc(&doc).is_err()); + assert!(w + .account_client() + .try_apply_doc(&doc, &no_recovery_evidence(&w.env)) + .is_err()); // Nothing was installed: the account still has only its constructor admin. assert_eq!(w.account_client().get_context_rules_count(), 1); } diff --git a/crates/integration-tests/tests/cap_matrix.rs b/crates/integration-tests/tests/cap_matrix.rs index e0cac82..603c367 100644 --- a/crates/integration-tests/tests/cap_matrix.rs +++ b/crates/integration-tests/tests/cap_matrix.rs @@ -69,6 +69,7 @@ fn cap_doc(pubkey_hex: &str) -> perch_ir::PolicyDoc { period_ledgers: PERIOD_LEDGERS, }), }], + recovery: None, } } diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs new file mode 100644 index 0000000..88402e3 --- /dev/null +++ b/crates/integration-tests/tests/recovery.rs @@ -0,0 +1,359 @@ +//! End-to-end account recovery, guardian-only mode: enrollment, initiation, +//! guardian quorum, the timelock, completion (Variant A — recovery +//! authorizes `apply_doc` itself, via the real OZ `do_check_auth` / +//! `Policy::enforce` path), replay/expiry/wrong-target refusal, and the +//! unconditional pending-attempt guard on ordinary `apply_doc` calls. See +//! `docs/recovery/controller-governance.md`. +//! +//! Setup (enrollment, evidence submission) goes through +//! `perch_testkit::Bootstrap`'s mocked-auth world — real auth *mechanics* +//! aren't what's under test there (`matrix.rs` already proves those). The +//! completion assertions instead call `do_check_auth` directly, matching +//! `matrix.rs`'s own pattern: under host-level auth mocking (recording mode), +//! the host never invokes a custom account's `__check_auth` at all (confirmed +//! against `soroban-env-host`'s own source — recording mode explicitly skips +//! it), so exercising `Policy::enforce` for real requires driving +//! `do_check_auth` directly with an explicit rule selection, exactly as +//! `__check_auth` itself does. + +use perch_recovery::{PerchRecovery, PerchRecoveryClient}; +use perch_testkit::{no_recovery_evidence, Bootstrap, World, FIXTURE_NETWORK}; +use soroban_sdk::auth::{Context, ContractContext}; +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{crypto::Hash, vec, Address, Bytes, BytesN, IntoVal, Map, Symbol}; +use stellar_accounts::smart_account::{do_check_auth, AuthPayload}; + +const ADMIN_VERIFIER: &str = "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN"; +const ADMIN_KEY: &str = "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e"; +const NEW_ADMIN_VERIFIER: &str = "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG"; +const NEW_ADMIN_KEY: &str = "1ce6040b0d03232ac6c911b0c375f1a52ebdefff56fd361d13680e23ca578a17"; + +fn setup() -> World { + Bootstrap::native() + .network(FIXTURE_NETWORK) + .admin_ed25519([9u8; 32]) + .build() +} + +fn strkey(a: &Address) -> std::string::String { + // `Address::to_string` returns a `soroban_sdk::String`; `Display` on + // that yields the strkey, which `.to_string()` again collects into a + // plain Rust `String` for building JSON. + a.to_string().to_string() +} + +fn enroll_doc(controller: &Address, guardians: &[Address], quorum: u32) -> std::string::String { + let guardian_list = guardians + .iter() + .map(|g| format!("\"{}\"", strkey(g))) + .collect::>() + .join(","); + format!( + r#"{{ + "version": 1, + "network": "{FIXTURE_NETWORK}", + "signers": [ + {{ "id": "admin", "verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}" }} + ], + "rules": [ + {{ "name": "admin", "scope": {{ "type": "self-admin" }}, + "principals": {{ "type": "all", "signers": ["admin"] }} }} + ], + "recovery": {{ + "profile": "loss", + "mode": {{ "type": "guardian-only", "guardians": [{guardian_list}], "quorum": {quorum} }}, + "controller": "{controller}", + "replaceable": ["admin"], + "delay-ledgers": 5, + "expiry-ledgers": 1000, + "max-cancels": 3, + "pending-activity": "continue" + }} +}}"#, + controller = strkey(controller), + ) +} + +/// The lost-key target: the enrolled document with the admin credential +/// replaced. Same recovery section — a legitimate completion never changes +/// enrolled configuration (see `guard_apply_doc`'s design). +fn target_doc(controller: &Address, guardians: &[Address], quorum: u32) -> std::string::String { + enroll_doc(controller, guardians, quorum).replace( + &format!(r#""verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}""#), + &format!(r#""verifier": "{NEW_ADMIN_VERIFIER}", "key": "{NEW_ADMIN_KEY}""#), + ) +} + +fn recovery_rule_id(w: &World) -> u32 { + let client = w.account_client(); + let n = client.get_context_rules_count(); + // ids are assigned sequentially and never reused; scan from the top. + let mut found = None; + for id in (1..=n + 10).rev() { + if let Ok(Ok(rule)) = client.try_get_context_rule(&id) { + if rule.name == soroban_sdk::String::from_str(&w.env, "recovery") { + found = Some(id); + break; + } + } + } + found.expect("recovery rule installed") +} + +/// Drive `do_check_auth` directly (matching `matrix.rs`), selecting `rule_id` +/// with zero signers — the shape a policy-only, zero-signer rule requires — +/// authorizing `account.apply_doc(doc_bytes)`. +fn complete_via_rule( + w: &World, + account: &Address, + rule_id: u32, + doc_bytes: &Bytes, +) -> Result<(), ()> { + let ctx = Context::Contract(ContractContext { + contract: account.clone(), + fn_name: Symbol::new(&w.env, "apply_doc"), + args: vec![ + &w.env, + doc_bytes.into_val(&w.env), + no_recovery_evidence(&w.env).into_val(&w.env), + ], + }); + let payload = AuthPayload { + signers: Map::new(&w.env), + context_rule_ids: vec![&w.env, rule_id], + }; + let hash: Hash<32> = w + .env + .crypto() + .sha256(&Bytes::from_array(&w.env, &[0x11; 32])); + let contexts = vec![&w.env, ctx]; + let account = account.clone(); + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + w.env.as_contract(&account, || { + do_check_auth(&w.env, &hash, &payload, &contexts).unwrap(); + }); + })) + .map_err(|_| ()) +} + +/// Enroll guardian-only 2-of-2 and begin a lost-key attempt against `target`, +/// returning `(world, controller_client, rule_id, target_bytes)`. Shared by +/// the completion-refusal tests below, each of which then does exactly one +/// `complete_via_rule` call — kept to one such call per test (rather than a +/// chain of expected-panic-then-continue calls in one `Env`) since Soroban's +/// test host's call-stack bookkeeping does not reliably survive multiple +/// `catch_unwind`-recovered host panics within a single `Env`. +fn setup_pending_attempt(quorum_approvals: &[bool]) -> (World, Address, u32, Bytes) { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + let doc = enroll_doc(&controller, &[g1.clone(), g2.clone()], 2); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let rule_id = recovery_rule_id(&w); + + let target = target_doc(&controller, &[g1.clone(), g2.clone()], 2); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + if quorum_approvals.first().copied().unwrap_or(false) { + recovery.submit_guardian_approval(&w.account, &g1); + } + if quorum_approvals.get(1).copied().unwrap_or(false) { + recovery.submit_guardian_approval(&w.account, &g2); + } + (w, controller, rule_id, target_bytes) +} + +#[test] +fn completion_refused_with_no_evidence_at_all() { + let (w, controller, rule_id, target_bytes) = setup_pending_attempt(&[false, false]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_err()); + assert!(recovery.has_pending(&w.account)); +} + +#[test] +fn completion_refused_below_guardian_quorum() { + let (w, controller, rule_id, target_bytes) = setup_pending_attempt(&[true, false]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + assert_eq!( + recovery.get_attempt(&w.account).unwrap().executable_after, + 0 + ); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_err()); + assert!(recovery.has_pending(&w.account)); +} + +#[test] +fn completion_refused_before_the_timelock_elapses() { + let (w, controller, rule_id, target_bytes) = setup_pending_attempt(&[true, true]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let attempt = recovery.get_attempt(&w.account).unwrap(); + assert!(attempt.executable_after > w.env.ledger().sequence()); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_err()); +} + +#[test] +fn completion_refused_for_the_wrong_target_document() { + let (w, controller, rule_id, _) = setup_pending_attempt(&[true, true]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + let wrong = Bytes::from_slice( + &w.env, + enroll_doc(&Address::generate(&w.env), &[], 1).as_bytes(), + ); + assert!(complete_via_rule(&w, &w.account, rule_id, &wrong).is_err()); + assert!(recovery.has_pending(&w.account)); // not consumed by the refusal +} + +#[test] +fn guardian_only_lost_key_recovery_completes_and_installs_the_target_document() { + let (w, controller, rule_id, target_bytes) = setup_pending_attempt(&[true, true]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_ok()); + assert!(!recovery.has_pending(&w.account)); + assert_eq!( + recovery.get_attempt(&w.account).unwrap().state, + perch_recovery::types::AttemptState::Completed + ); +} + +#[test] +fn completion_cannot_be_replayed() { + let (w, controller, rule_id, target_bytes) = setup_pending_attempt(&[true, true]); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_ok()); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_err()); +} + +#[test] +fn guardian_quorum_alone_never_promotes_without_being_reached() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + let g3 = Address::generate(&w.env); + + let doc = enroll_doc(&controller, &[g1.clone(), g2.clone(), g3.clone()], 3); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target = target_doc(&controller, &[g1.clone(), g2.clone(), g3.clone()], 3); + let target_hash: BytesN<32> = w + .env + .crypto() + .sha256(&Bytes::from_slice(&w.env, target.as_bytes())) + .to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + recovery.submit_guardian_approval(&w.account, &g1); + recovery.submit_guardian_approval(&w.account, &g2); + // 2 of 3: not yet quorum. + assert_eq!( + recovery.get_attempt(&w.account).unwrap().executable_after, + 0 + ); + + recovery.submit_guardian_approval(&w.account, &g3); + assert!(recovery.get_attempt(&w.account).unwrap().executable_after > 0); +} + +#[test] +fn guardian_cancel_is_a_separate_domain_from_initiation_approval() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + let doc = enroll_doc(&controller, &[g1.clone(), g2.clone()], 2); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target = target_doc(&controller, &[g1.clone(), g2.clone()], 2); + let target_hash: BytesN<32> = w + .env + .crypto() + .sha256(&Bytes::from_slice(&w.env, target.as_bytes())) + .to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + // g1's INITIATION approval does not count toward cancellation. + recovery.submit_guardian_approval(&w.account, &g1); + recovery.submit_guardian_cancel(&w.account, &g1); + assert!(recovery.has_pending(&w.account)); // only 1 of 2 cancel votes + + recovery.submit_guardian_cancel(&w.account, &g2); + assert!(!recovery.has_pending(&w.account)); // cancelled + + // A fresh attempt can be started after cancellation. + let id2 = recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + assert_eq!(id2, 1); +} + +#[test] +fn protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + let protected_doc = enroll_doc(&controller, &[g1.clone(), g2.clone()], 2) + .replace("\"profile\": \"loss\"", "\"profile\": \"protected\""); + let client = w.account_client(); + client.apply_doc( + &Bytes::from_slice(&w.env, protected_doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + + // Disable recovery entirely (omit the `recovery` field) with ordinary + // admin authorization only — must be refused: an admin key alone cannot + // downgrade or disable `Protected` recovery. + let plain_doc = format!( + r#"{{ + "version": 1, + "network": "{FIXTURE_NETWORK}", + "signers": [ + {{ "id": "admin", "verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}" }} + ], + "rules": [ + {{ "name": "admin", "scope": {{ "type": "self-admin" }}, + "principals": {{ "type": "all", "signers": ["admin"] }} }} + ] +}}"# + ); + assert!(client + .try_apply_doc( + &Bytes::from_slice(&w.env, plain_doc.as_bytes()), + &no_recovery_evidence(&w.env) + ) + .is_err()); + // Nothing changed: still enrolled. + let recovery = PerchRecoveryClient::new(&w.env, &controller); + assert!(recovery.get_config(&w.account).is_some()); +} diff --git a/crates/perch-conformance/tests/differential.rs b/crates/perch-conformance/tests/differential.rs index 1e5c466..8899628 100644 --- a/crates/perch-conformance/tests/differential.rs +++ b/crates/perch-conformance/tests/differential.rs @@ -223,6 +223,7 @@ fn gen_doc(rng: &mut Rng, doc_idx: u32) -> PolicyDoc { }, signers, rules, + recovery: None, } } diff --git a/crates/perch-doc-compiler/src/lib.rs b/crates/perch-doc-compiler/src/lib.rs index 1bb1f00..1f4f220 100644 --- a/crates/perch-doc-compiler/src/lib.rs +++ b/crates/perch-doc-compiler/src/lib.rs @@ -100,6 +100,87 @@ pub struct CompiledDoc { /// minified twin compile to the same hash. pub doc_hash: BytesN<32>, pub rules: Vec, + /// Zero or one entries (a `Vec` for the same ScVal reason as + /// [`CompiledRule::install`]). Present ⇒ the document enrolls account + /// recovery — the applier is expected to sync this configuration to the + /// adopted recovery-controller instance named in it. See + /// `docs/recovery/` for the full design; wire-compat notes live there too + /// (this is a new field on an existing constructorless, immutable + /// deployable — a compiler build carrying it is a new instance, not an + /// in-place change to any already-deployed one). + pub recovery: Vec, +} + +/// Wire form of [`perch_ir::RecoveryConfig`]: resolved addresses and decoded +/// bytes, exactly as [`CompiledRule`] is to [`perch_ir::Rule`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct CompiledRecoveryConfig { + pub profile: RecoveryProfile, + pub mode: CompiledRecoveryMode, + pub controller: Address, + /// Zero or one entries (a `Vec` for the same ScVal reason as + /// [`CompiledRule::install`]). Present ⇒ suspected-compromise recovery is + /// enrolled, restoring the document this hash names. + pub baseline: Vec>, + /// Fingerprint of each replaceable signer's *physical credential* + /// (`sha256` of a tagged encoding of its `SignerMethod` — verifier+key for + /// `external`, the address for `delegated`), resolved from + /// `doc.signers` at compile time — not the document-local signer id + /// string. Revocation must survive the id being reused for a different + /// physical key in a later document, so the controller tracks the + /// credential itself. + pub replaceable: Vec>, + pub delay_ledgers: u32, + pub expiry_ledgers: u32, + pub max_cancels: u32, + pub pending_activity: PendingActivityPolicy, +} + +/// Wire form of [`perch_ir::RecoveryProfile`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum RecoveryProfile { + Loss, + Protected, +} + +/// Wire form of [`perch_ir::PendingActivityPolicy`]. No default, same as the +/// document-level type — see its doc comment. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum PendingActivityPolicy { + Freeze, + Continue, +} + +/// Wire form of [`perch_ir::RecoveryMode`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum CompiledRecoveryMode { + GuardianOnly(CompiledGuardianSet), + ZkOnly(CompiledZkVerifierConfig), + Combined(CompiledGuardianSet, CompiledZkVerifierConfig), +} + +/// Wire form of [`perch_ir::GuardianSet`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct CompiledGuardianSet { + pub guardians: Vec
, + pub quorum: u32, +} + +/// Wire form of [`perch_ir::ZkVerifierConfig`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct CompiledZkVerifierConfig { + pub verifier: Address, + /// Decoded from the document's hex `circuit-id`. + pub circuit_id: Bytes, + /// Zero or one entries (a `Vec` for the same ScVal reason as + /// [`CompiledRule::install`]). + pub pool: Vec
, } /// Cross-contract client, generated independently of the deployable so @@ -161,10 +242,134 @@ impl PerchDocCompiler { .crypto() .sha256(&Bytes::from_slice(e, canonical.as_bytes())) .to_bytes(); - Ok(CompiledDoc { doc_hash, rules }) + + let mut recovery: Vec = Vec::new(e); + if let Some(r) = &doc.recovery { + recovery.push_back(to_compiled_recovery(e, &doc, r)?); + } + + Ok(CompiledDoc { + doc_hash, + rules, + recovery, + }) } } +/// Lower a validated [`perch_ir::RecoveryConfig`] to its wire form. +/// Precondition: `perch_ir::validate(doc).is_ok()` (guaranteed by the one +/// caller, [`PerchDocCompiler::compile_doc`]) — every address is shape-valid +/// and `circuit_id`/baseline `doc_hash` are valid hex of the expected length, +/// so the decodes below cannot fail on a document that reached this point. +#[cfg(feature = "contract")] +fn to_compiled_recovery( + e: &Env, + doc: &perch_ir::PolicyDoc, + r: &perch_ir::RecoveryConfig, +) -> Result { + let profile = match r.profile { + perch_ir::RecoveryProfile::Loss => RecoveryProfile::Loss, + perch_ir::RecoveryProfile::Protected => RecoveryProfile::Protected, + }; + let pending_activity = match r.pending_activity { + perch_ir::PendingActivityPolicy::Freeze => PendingActivityPolicy::Freeze, + perch_ir::PendingActivityPolicy::Continue => PendingActivityPolicy::Continue, + }; + let mode = match &r.mode { + perch_ir::RecoveryMode::GuardianOnly(g) => { + CompiledRecoveryMode::GuardianOnly(to_compiled_guardian_set(e, g)) + } + perch_ir::RecoveryMode::ZkOnly(z) => { + CompiledRecoveryMode::ZkOnly(to_compiled_zk_verifier_config(e, z)?) + } + perch_ir::RecoveryMode::Combined(g, z) => CompiledRecoveryMode::Combined( + to_compiled_guardian_set(e, g), + to_compiled_zk_verifier_config(e, z)?, + ), + }; + let mut baseline: Vec> = Vec::new(e); + if let Some(b) = &r.baseline { + baseline.push_back(hex_bytes_32(e, &b.doc_hash)?); + } + // Precondition (validate(doc).is_ok(), guaranteed by the one caller): + // every `replaceable` id references a declared signer + // (`UnknownRecoveryReplaceableRef` would already have failed validation), + // so `.find` cannot miss here. + let mut replaceable: Vec> = Vec::new(e); + for id in &r.replaceable { + let decl = doc + .signers + .iter() + .find(|s| &s.id == id) + .ok_or(DocCompilerError::DocInvalid)?; + replaceable.push_back(credential_fingerprint(e, &decl.method)); + } + Ok(CompiledRecoveryConfig { + profile, + mode, + controller: Address::from_str(e, &r.controller), + baseline, + replaceable, + delay_ledgers: r.delay_ledgers, + expiry_ledgers: r.expiry_ledgers, + max_cancels: r.max_cancels, + pending_activity, + }) +} + +/// `sha256` of a tagged encoding of a signer's physical credential — the +/// verifier+key pair for `external`, the address for `delegated`. Used only +/// to fingerprint a `replaceable` signer's credential identity, never the +/// document-local id string, so revocation survives that id being reused for +/// a different physical key in a later document. +#[cfg(feature = "contract")] +fn credential_fingerprint(e: &Env, method: &perch_ir::SignerMethod) -> BytesN<32> { + let mut buf = alloc::string::String::new(); + match method { + perch_ir::SignerMethod::External { verifier, key } => { + buf.push_str("external|"); + buf.push_str(verifier); + buf.push('|'); + buf.push_str(key); + } + perch_ir::SignerMethod::Delegated { address } => { + buf.push_str("delegated|"); + buf.push_str(address); + } + } + e.crypto() + .sha256(&Bytes::from_slice(e, buf.as_bytes())) + .to_bytes() +} + +#[cfg(feature = "contract")] +fn to_compiled_guardian_set(e: &Env, g: &perch_ir::GuardianSet) -> CompiledGuardianSet { + let mut guardians: Vec
= Vec::new(e); + for addr in &g.guardians { + guardians.push_back(Address::from_str(e, addr)); + } + CompiledGuardianSet { + guardians, + quorum: g.quorum, + } +} + +#[cfg(feature = "contract")] +fn to_compiled_zk_verifier_config( + e: &Env, + z: &perch_ir::ZkVerifierConfig, +) -> Result { + let mut pool: Vec
= Vec::new(e); + if let Some(p) = &z.pool { + pool.push_back(Address::from_str(e, p)); + } + Ok(CompiledZkVerifierConfig { + verifier: Address::from_str(e, &z.verifier), + circuit_id: hex_bytes(e, &z.circuit_id)?, + pool, + }) +} + /// Lowered rule (host-independent strings) → wire rule (resolved soroban /// types). #[cfg(feature = "contract")] @@ -227,3 +432,28 @@ fn hex_bytes(e: &Env, s: &str) -> Result { } Ok(Bytes::from_slice(e, &out)) } + +/// Decode a validated 64-hex-character string (a SHA-256 digest, e.g. a +/// recovery baseline's `doc-hash`) into fixed 32 bytes. Validation already +/// guarantees the length and hex-ness; still fail closed here rather than +/// trust it. +#[cfg(feature = "contract")] +fn hex_bytes_32(e: &Env, s: &str) -> Result, DocCompilerError> { + let b = s.as_bytes(); + if b.len() != 64 { + return Err(DocCompilerError::DocInvalid); + } + let nib = |c: u8| -> Result { + match c { + b'0'..=b'9' => Ok(c - b'0'), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'A'..=b'F' => Ok(c - b'A' + 10), + _ => Err(DocCompilerError::DocInvalid), + } + }; + let mut out = [0u8; 32]; + for (i, chunk) in out.iter_mut().enumerate() { + *chunk = (nib(b[2 * i])? << 4) | nib(b[2 * i + 1])?; + } + Ok(BytesN::from_array(e, &out)) +} diff --git a/crates/perch-ir/src/canon.rs b/crates/perch-ir/src/canon.rs index e41447c..ac29c12 100644 --- a/crates/perch-ir/src/canon.rs +++ b/crates/perch-ir/src/canon.rs @@ -29,7 +29,9 @@ //! format version. use crate::doc::{ - ArgPred, CapConstraint, PolicyDoc, Principals, Rule, Scope, SignerDecl, SignerMethod, + ArgPred, CapConstraint, GuardianSet, PendingActivityPolicy, PolicyDoc, Principals, + RecoveryConfig, RecoveryMode, RecoveryProfile, Rule, Scope, SignerDecl, SignerMethod, + ZkVerifierConfig, }; #[cfg(not(feature = "std"))] use alloc::{ @@ -110,9 +112,88 @@ fn doc_to_cv(doc: &PolicyDoc) -> Cv<'_> { Cv::Arr(doc.signers.iter().map(signer_to_cv).collect()), )); o.push(("rules", Cv::Arr(doc.rules.iter().map(rule_to_cv).collect()))); + if let Some(r) = &doc.recovery { + o.push(("recovery", recovery_to_cv(r))); + } Cv::Obj(o) } +fn recovery_to_cv(r: &RecoveryConfig) -> Cv<'_> { + let mut o: Vec<(&'static str, Cv)> = Vec::new(); + o.push(("profile", Cv::Str(recovery_profile_str(r.profile)))); + o.push(("mode", recovery_mode_to_cv(&r.mode))); + o.push(("controller", Cv::Str(&r.controller))); + if let Some(b) = &r.baseline { + o.push(( + "baseline", + Cv::Obj(vec![("doc-hash", Cv::Str(&b.doc_hash))]), + )); + } + o.push(( + "replaceable", + Cv::Arr(r.replaceable.iter().map(|s| Cv::Str(s)).collect()), + )); + o.push(("delay-ledgers", Cv::U32(r.delay_ledgers))); + o.push(("expiry-ledgers", Cv::U32(r.expiry_ledgers))); + o.push(("max-cancels", Cv::U32(r.max_cancels))); + o.push(( + "pending-activity", + Cv::Str(pending_activity_str(r.pending_activity)), + )); + Cv::Obj(o) +} + +fn recovery_profile_str(p: RecoveryProfile) -> &'static str { + match p { + RecoveryProfile::Loss => "loss", + RecoveryProfile::Protected => "protected", + } +} + +fn pending_activity_str(p: PendingActivityPolicy) -> &'static str { + match p { + PendingActivityPolicy::Freeze => "freeze", + PendingActivityPolicy::Continue => "continue", + } +} + +fn recovery_mode_to_cv(mode: &RecoveryMode) -> Cv<'_> { + match mode { + RecoveryMode::GuardianOnly(g) => { + let mut o = vec![("type", Cv::Str("guardian-only"))]; + push_guardian_fields(&mut o, g); + Cv::Obj(o) + } + RecoveryMode::ZkOnly(z) => { + let mut o = vec![("type", Cv::Str("zk-only"))]; + push_zk_fields(&mut o, z); + Cv::Obj(o) + } + RecoveryMode::Combined(g, z) => { + let mut o = vec![("type", Cv::Str("combined"))]; + push_guardian_fields(&mut o, g); + push_zk_fields(&mut o, z); + Cv::Obj(o) + } + } +} + +fn push_guardian_fields<'a>(o: &mut Vec<(&'static str, Cv<'a>)>, g: &'a GuardianSet) { + o.push(( + "guardians", + Cv::Arr(g.guardians.iter().map(|s| Cv::Str(s)).collect()), + )); + o.push(("quorum", Cv::U32(g.quorum))); +} + +fn push_zk_fields<'a>(o: &mut Vec<(&'static str, Cv<'a>)>, z: &'a ZkVerifierConfig) { + o.push(("verifier", Cv::Str(&z.verifier))); + o.push(("circuit-id", Cv::Str(&z.circuit_id))); + if let Some(pool) = &z.pool { + o.push(("pool", Cv::Str(pool))); + } +} + fn signer_to_cv(s: &SignerDecl) -> Cv<'_> { match &s.method { SignerMethod::External { verifier, key } => Cv::Obj(vec![ diff --git a/crates/perch-ir/src/doc.rs b/crates/perch-ir/src/doc.rs index b18e83e..fb23706 100644 --- a/crates/perch-ir/src/doc.rs +++ b/crates/perch-ir/src/doc.rs @@ -36,6 +36,12 @@ pub struct PolicyDoc { pub signers: Vec, /// The policy rules. Order is preserved and significant to the hash. pub rules: Vec, + /// Opt-in account-recovery enrollment: restoring access after key loss, or + /// an approved baseline after suspected admin compromise. Omitted from the + /// canonical form when `None`, so a document that never enrolls recovery + /// hashes exactly as it did before this field existed (see `CANONICAL.md` + /// and the `ci-publish-recovery` conformance vector). + pub recovery: Option, } /// A declared signer: an id local to the document plus how it authenticates. @@ -328,3 +334,165 @@ pub struct U32EqPred { /// The exact u32 value the argument must equal. pub value: u32, } + +/// Opt-in account-recovery enrollment. This is *reviewable configuration* — +/// who may recover this account and how — never the recovery attempt itself; +/// an attempt's target document and evidence are supplied when recovery +/// starts, not predeclared here. Full design rationale lives in +/// `docs/recovery/`. +/// +/// The whole document's `doc_hash` already covers every field here (recovery +/// configuration is authority-bearing data like any other field), so no +/// separate top-level commitment is needed in the document itself. A deployed +/// recovery controller separately commits to its own `config_hash` scoped to +/// just the compiled form of this sub-object, for on-chain comparison without +/// re-hashing the whole document. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecoveryConfig { + /// Who may change this configuration going forward. A `Protected` + /// downgrade requires the currently enrolled recovery condition in + /// addition to ordinary admin authorization; `Loss` does not. See + /// `docs/recovery/controller-governance.md`. + pub profile: RecoveryProfile, + /// How a recovery attempt is authorized: guardians, ZK, or both against + /// the same proposal. Carries the mode-specific configuration directly, + /// so a `guardian-only` document has no ZK-shaped field anywhere to leave + /// unset — guardian-only recovery requires no ZK machinery to exist, + /// verify, or even compile in. + pub mode: RecoveryMode, + /// The recovery controller instance this account has adopted (a + /// constructorless, immutable contract's address, C-address strkey). + /// "Upgrading" the controller means adopting a different, newly deployed + /// immutable instance here through a new applied document — never a code + /// change at this address. See `docs/recovery/vk-and-controller-immutability.md`. + pub controller: String, + /// Commitment to the approved baseline document that suspected-compromise + /// recovery restores. Required to enroll suspected-compromise recovery; + /// `None` restricts enrollment to lost-key recovery only. + pub baseline: Option, + /// Which declared signer ids (`doc.signers[].id`) a recovery attempt may + /// replace. Must be non-empty and reference declared signers — recovery + /// enrolled with nothing to replace can never restore access. + pub replaceable: Vec, + /// Minimum ledgers between an attempt becoming authorized (its evidence + /// satisfied) and it becoming completable — the timelock window a + /// legitimate owner has to notice and cancel. A ledger-sequence delta, + /// like [`Rule::not_after_ledger`], not a duration in seconds. Must be + /// non-zero. + pub delay_ledgers: u32, + /// Ledgers after an attempt becomes authorized at which it lapses back to + /// requiring a fresh attempt (see `docs/recovery/` for the full state + /// machine). Must be non-zero. + pub expiry_ledgers: u32, + /// Cap on cancellations across this account's lifetime, bounding a + /// cancel-then-reattempt griefing cycle. Must be non-zero — a cap of zero + /// would forbid cancellation entirely, contradicting the guarantee that + /// the enrolled condition can always cancel a live attempt. + pub max_cancels: u32, + /// What happens to ordinary account-authorized execution while a recovery + /// attempt is pending. Deliberately has **no default** and no third + /// "unspecified" variant — every enrollment must name one explicitly. + /// This remains a release-blocking, explicitly recorded open decision; + /// see `docs/recovery/section-7-gate.md`. The field exists so the choice + /// is reviewable, inspectable configuration — not a library default. + pub pending_activity: PendingActivityPolicy, +} + +/// Routine recovery-configuration change authority for a [`RecoveryConfig`]. +/// Orthogonal to [`RecoveryMode`]: any mode may pair with either profile. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecoveryProfile { + /// Ordinary admin can change or disable recovery on its own. Recovers + /// from accidental key loss while enrollment remains valid; makes no + /// promise that a compromised admin cannot disable it. + Loss, + /// Ordinary admin plus the currently enrolled recovery condition are both + /// required to change or disable recovery. A stolen admin key alone + /// cannot downgrade or remove protection. + Protected, +} + +/// How a recovery attempt is authorized. Tagged with `"type"`: +/// `"guardian-only"`, `"zk-only"`, or `"combined"`. Combined requires *both* +/// factors against the same proposal, never either alone. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RecoveryMode { + /// Guardian quorum only. No ZK secret, Merkle membership, or proof + /// generation is involved anywhere in this mode. + GuardianOnly(GuardianSet), + /// Zero-knowledge proof only. No independent guardian defense exists + /// against compromise of the ZK recovery factor in this mode. + ZkOnly(ZkVerifierConfig), + /// Both a valid proof and guardian quorum are required, checked against + /// the same proposal. + Combined(GuardianSet, ZkVerifierConfig), +} + +/// An M-of-N guardian quorum: independent principals (not document signers) +/// that approve a recovery proposal by their own on-chain authorization. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GuardianSet { + /// Guardian addresses (G- or C-address strkey). Must be non-empty and + /// contain no address twice. + pub guardians: Vec, + /// How many of `guardians` must authorize. Must satisfy + /// `1 <= quorum <= guardians.len()`. + pub quorum: u32, +} + +/// The zero-knowledge factor of a [`RecoveryMode`]: which constructorless +/// verifier instance checks proofs, and which circuit/proof-format identity +/// that instance's immutable artifact commits to. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ZkVerifierConfig { + /// The verifier contract's address (C-address strkey). Constructorless + /// and immutable: its code, and the verification key it embeds, never + /// change at this address. An "upgrade" is enrolling a different, newly + /// deployed verifier address here. + pub verifier: String, + /// Hex-encoded circuit/proof-format identity the verifier's immutable + /// artifact commits to. Defense-in-depth binding: a proof for a + /// different circuit cannot be substituted even if a verifier address + /// were ever confused with another. + pub circuit_id: String, + /// A membership-pool contract's address (C-address strkey), for ZK + /// schemes that prove membership of a secret in a set rather than + /// knowledge of one fixed secret. `None` for schemes with no pool. + pub pool: Option, +} + +/// Commitment to a previously-approved policy document that suspected- +/// compromise recovery restores (with designated credentials replaced). +/// +/// Deliberately just a pointer to a *different* document's identity, never +/// the enclosing document's own hash, so the commitment cannot recursively +/// contain itself: the baseline this document points to was applied (and +/// hashed) before this document existed, and can never be this document. See +/// `docs/recovery/` (the "reviewable configuration without circular hashes" +/// requirement). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaselineCommitment { + /// Canonical `doc_hash` (lowercase hex) of the previously-applied policy + /// document that suspected-compromise recovery restores. Declared, + /// reviewer-checked data — not verified against on-chain history by the + /// compiler, since accounts do not retain applied-document history (only + /// the current `applied_doc_hash`). Reviewing that this hash names a + /// real, previously-approved document is part of enrollment review. + pub doc_hash: String, +} + +/// Whether ordinary account-authorized execution continues, or is frozen, +/// while a recovery attempt is pending. There is deliberately no `Default` +/// impl and no third "unspecified" variant — every [`RecoveryConfig`] must +/// name one explicitly (enforced by [`crate::parse::from_json`] requiring the +/// field). See [`RecoveryConfig::pending_activity`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PendingActivityPolicy { + /// Ordinary account-authorized execution is blocked while an attempt is + /// pending (from authorized evidence through completion, cancellation, + /// or expiry). + Freeze, + /// Ordinary account-authorized execution continues unimpeded while an + /// attempt is pending. + Continue, +} diff --git a/crates/perch-ir/src/lib.rs b/crates/perch-ir/src/lib.rs index f4bf48b..e43d3be 100644 --- a/crates/perch-ir/src/lib.rs +++ b/crates/perch-ir/src/lib.rs @@ -72,9 +72,11 @@ pub use canon::{canonical_json, CANON_VERSION}; #[cfg(feature = "std")] pub use canon::{doc_hash, doc_hash_hex}; pub use doc::{ - AddressEqPred, AllPrincipals, ArgConstraint, ArgPred, CapConstraint, ContractScope, IsSelfPred, - PolicyDoc, Principals, Rule, Scope, SelfAdminScope, SelfAuthenticatingPrincipals, SignerDecl, - SignerMethod, StringInPred, StringPrefixPred, ThresholdPrincipals, U32EqPred, + AddressEqPred, AllPrincipals, ArgConstraint, ArgPred, BaselineCommitment, CapConstraint, + ContractScope, GuardianSet, IsSelfPred, PendingActivityPolicy, PolicyDoc, Principals, + RecoveryConfig, RecoveryMode, RecoveryProfile, Rule, Scope, SelfAdminScope, + SelfAuthenticatingPrincipals, SignerDecl, SignerMethod, StringInPred, StringPrefixPred, + ThresholdPrincipals, U32EqPred, ZkVerifierConfig, }; pub use parse::{from_json, JsonError, ParseError}; pub use validate::{ diff --git a/crates/perch-ir/src/parse.rs b/crates/perch-ir/src/parse.rs index ea49ee1..e0d826d 100644 --- a/crates/perch-ir/src/parse.rs +++ b/crates/perch-ir/src/parse.rs @@ -52,9 +52,11 @@ use hifijson::value::{self, Value}; use hifijson::SliceLexer; use crate::doc::{ - AddressEqPred, AllPrincipals, ArgConstraint, ArgPred, CapConstraint, ContractScope, IsSelfPred, - PolicyDoc, Principals, Rule, Scope, SelfAdminScope, SelfAuthenticatingPrincipals, SignerDecl, - SignerMethod, StringInPred, StringPrefixPred, ThresholdPrincipals, U32EqPred, + AddressEqPred, AllPrincipals, ArgConstraint, ArgPred, BaselineCommitment, CapConstraint, + ContractScope, GuardianSet, IsSelfPred, PendingActivityPolicy, PolicyDoc, Principals, + RecoveryConfig, RecoveryMode, RecoveryProfile, Rule, Scope, SelfAdminScope, + SelfAuthenticatingPrincipals, SignerDecl, SignerMethod, StringInPred, StringPrefixPred, + ThresholdPrincipals, U32EqPred, ZkVerifierConfig, }; /// Maximum JSON nesting depth accepted by [`from_json`]. A valid `PolicyDoc` @@ -285,7 +287,7 @@ fn tag>(obj: Members<'_, N, S>, ctx: &str) -> Result, S: AsRef>(v: &Value) -> Result { let obj = as_object(v, "document")?; - deny_unknown(obj, &["version", "network", "signers", "rules"])?; + deny_unknown(obj, &["version", "network", "signers", "rules", "recovery"])?; // The version pre-check in `from_json` has already rejected any numeric // `version != 1`; here it must be the integer `1`, a duplicate (caught by // `req_field`), or a non-number (caught by `as_u32` as "expected u32"). @@ -302,11 +304,16 @@ fn doc_from_value, S: AsRef>(v: &Value) -> Result, _>>()?; + let recovery = match opt_field(obj, "recovery")? { + Some(v) => Some(recovery_from_value(v)?), + None => None, + }; Ok(PolicyDoc { version, network, signers, rules, + recovery, }) } @@ -518,3 +525,146 @@ fn cap_from_value, S: AsRef>( period_ledgers: as_u32(req_field(obj, "period-ledgers")?, "period-ledgers")?, }) } + +// --- recovery configuration --------------------------------------------------- + +fn recovery_from_value, S: AsRef>( + v: &Value, +) -> Result { + let obj = as_object(v, "recovery")?; + deny_unknown( + obj, + &[ + "profile", + "mode", + "controller", + "baseline", + "replaceable", + "delay-ledgers", + "expiry-ledgers", + "max-cancels", + "pending-activity", + ], + )?; + let profile = recovery_profile_from_value(req_field(obj, "profile")?)?; + let mode = recovery_mode_from_value(req_field(obj, "mode")?)?; + let controller = as_str(req_field(obj, "controller")?, "recovery controller")?; + let baseline = match opt_field(obj, "baseline")? { + Some(v) => Some(baseline_commitment_from_value(v)?), + None => None, + }; + let replaceable = as_array(req_field(obj, "replaceable")?, "recovery replaceable")? + .iter() + .map(|e| as_str(e, "replaceable signer id")) + .collect::, _>>()?; + let delay_ledgers = as_u32(req_field(obj, "delay-ledgers")?, "recovery delay-ledgers")?; + let expiry_ledgers = as_u32(req_field(obj, "expiry-ledgers")?, "recovery expiry-ledgers")?; + let max_cancels = as_u32(req_field(obj, "max-cancels")?, "recovery max-cancels")?; + let pending_activity = pending_activity_policy_from_value(req_field(obj, "pending-activity")?)?; + Ok(RecoveryConfig { + profile, + mode, + controller, + baseline, + replaceable, + delay_ledgers, + expiry_ledgers, + max_cancels, + pending_activity, + }) +} + +fn recovery_profile_from_value>( + v: &Value, +) -> Result { + match as_str(v, "recovery profile")?.as_str() { + "loss" => Ok(RecoveryProfile::Loss), + "protected" => Ok(RecoveryProfile::Protected), + other => Err(json_err(format!( + "unknown value `{other}` for recovery profile" + ))), + } +} + +fn recovery_mode_from_value, S: AsRef>( + v: &Value, +) -> Result { + let obj = as_object(v, "recovery mode")?; + match tag(obj, "recovery mode type")?.as_str() { + "guardian-only" => { + deny_unknown(obj, &["type", "guardians", "quorum"])?; + Ok(RecoveryMode::GuardianOnly(guardian_set_from_fields(obj)?)) + } + "zk-only" => { + deny_unknown(obj, &["type", "verifier", "circuit-id", "pool"])?; + Ok(RecoveryMode::ZkOnly(zk_verifier_config_from_fields(obj)?)) + } + "combined" => { + deny_unknown( + obj, + &[ + "type", + "guardians", + "quorum", + "verifier", + "circuit-id", + "pool", + ], + )?; + Ok(RecoveryMode::Combined( + guardian_set_from_fields(obj)?, + zk_verifier_config_from_fields(obj)?, + )) + } + other => Err(json_err(format!( + "unknown variant `{other}` for recovery mode type" + ))), + } +} + +fn guardian_set_from_fields, S: AsRef>( + obj: Members<'_, N, S>, +) -> Result { + Ok(GuardianSet { + guardians: as_array(req_field(obj, "guardians")?, "guardians")? + .iter() + .map(|e| as_str(e, "guardian address")) + .collect::, _>>()?, + quorum: as_u32(req_field(obj, "quorum")?, "guardian quorum")?, + }) +} + +fn zk_verifier_config_from_fields, S: AsRef>( + obj: Members<'_, N, S>, +) -> Result { + Ok(ZkVerifierConfig { + verifier: as_str(req_field(obj, "verifier")?, "zk verifier")?, + circuit_id: as_str(req_field(obj, "circuit-id")?, "zk circuit-id")?, + pool: match opt_field(obj, "pool")? { + Some(v) => Some(as_str(v, "zk pool")?), + None => None, + }, + }) +} + +fn baseline_commitment_from_value, S: AsRef>( + v: &Value, +) -> Result { + let obj = as_object(v, "recovery baseline")?; + deny_unknown(obj, &["doc-hash"])?; + Ok(BaselineCommitment { + doc_hash: as_str(req_field(obj, "doc-hash")?, "baseline doc-hash")?, + }) +} + +fn pending_activity_policy_from_value>( + v: &Value, +) -> Result { + match as_str(v, "recovery pending-activity")?.as_str() { + "freeze" => Ok(PendingActivityPolicy::Freeze), + "continue" => Ok(PendingActivityPolicy::Continue), + other => Err(json_err(format!( + "unknown value `{other}` for recovery pending-activity" + ))), + } +} diff --git a/crates/perch-ir/src/validate.rs b/crates/perch-ir/src/validate.rs index c67fdea..02f8611 100644 --- a/crates/perch-ir/src/validate.rs +++ b/crates/perch-ir/src/validate.rs @@ -18,7 +18,10 @@ //! ([`ValidationError::DuplicatePrincipalSigner`]), and repeated values in a //! `string-in` predicate ([`ValidationError::DuplicateStringInValue`]). -use crate::doc::{ArgPred, PolicyDoc, Principals, Rule, Scope, SignerMethod}; +use crate::doc::{ + ArgPred, GuardianSet, PolicyDoc, Principals, RecoveryConfig, RecoveryMode, Rule, Scope, + SignerMethod, ZkVerifierConfig, +}; use alloc::collections::{btree_map::Entry, BTreeMap, BTreeSet}; #[cfg(not(feature = "std"))] use alloc::{ @@ -277,6 +280,83 @@ pub enum ValidationError { /// The rule's contract scope address. scope: String, }, + /// `recovery.replaceable` is empty. Recovery enrolled with nothing to + /// replace can never restore access. + EmptyRecoveryReplaceable, + /// `recovery.replaceable` lists the same signer id twice. + DuplicateRecoveryReplaceable { + /// The duplicated signer id. + id: String, + }, + /// `recovery.replaceable` references a signer id that is not declared. + UnknownRecoveryReplaceableRef { + /// The undeclared signer id that was referenced. + id: String, + }, + /// `recovery.delay-ledgers` is `0`, which would make the timelock + /// meaningless (an attempt would be completable the instant its evidence + /// is satisfied). + ZeroRecoveryDelayLedgers, + /// `recovery.expiry-ledgers` is `0`, which would make an authorized + /// attempt expire immediately. + ZeroRecoveryExpiryLedgers, + /// `recovery.max-cancels` is `0`, which would forbid cancellation + /// entirely — the enrolled condition must always be able to cancel a + /// live attempt. + ZeroRecoveryMaxCancels, + /// `recovery.controller` is not shaped like a C-address strkey (checksum + /// not verified — see module docs). + InvalidRecoveryController { + /// The malformed address string. + address: String, + }, + /// `recovery.baseline.doc-hash` is not 64 lowercase hex characters (a + /// SHA-256 digest). + InvalidBaselineDocHash { + /// The malformed hash string. + doc_hash: String, + }, + /// A recovery mode's guardian set is empty. Guardian-based recovery with + /// no guardians can never satisfy quorum. + EmptyGuardianSet, + /// A recovery mode's guardian set lists the same address twice. + DuplicateGuardian { + /// The duplicated guardian address. + address: String, + }, + /// A recovery mode's guardian address is not shaped like a C- or + /// G-address strkey (checksum not verified — see module docs). + InvalidGuardianAddress { + /// The malformed address string. + address: String, + }, + /// A recovery mode's guardian `quorum` is out of range. Must satisfy + /// `1 <= quorum <= guardians.len()`, mirroring [`ValidationError::InvalidThreshold`]'s + /// rationale: zero would authorize with no guardian approvals, and a + /// quorum above the guardian count could never be met. + InvalidGuardianQuorum { + /// The declared quorum. + quorum: u32, + /// The guardian count (the N). + n: u32, + }, + /// A recovery mode's `verifier` is not shaped like a C-address strkey + /// (checksum not verified — see module docs). + InvalidRecoveryVerifier { + /// The malformed address string. + address: String, + }, + /// A recovery mode's `circuit-id` is not valid non-empty hex. + InvalidCircuitId { + /// The malformed circuit id string. + circuit_id: String, + }, + /// A recovery mode's `pool` is not shaped like a C-address strkey + /// (checksum not verified — see module docs). + InvalidRecoveryPool { + /// The malformed address string. + address: String, + }, } impl fmt::Display for ValidationError { @@ -397,6 +477,51 @@ impl fmt::Display for ValidationError { f, "rule `{rule}`: cap token `{token}` differs from the contract scope `{scope}` (omit it or set it equal)" ), + E::EmptyRecoveryReplaceable => write!( + f, + "recovery: replaceable is empty (nothing for recovery to restore)" + ), + E::DuplicateRecoveryReplaceable { id } => { + write!(f, "recovery: replaceable repeats signer `{id}`") + } + E::UnknownRecoveryReplaceableRef { id } => write!( + f, + "recovery: replaceable references undeclared signer `{id}`" + ), + E::ZeroRecoveryDelayLedgers => write!(f, "recovery: delay-ledgers is 0"), + E::ZeroRecoveryExpiryLedgers => write!(f, "recovery: expiry-ledgers is 0"), + E::ZeroRecoveryMaxCancels => write!(f, "recovery: max-cancels is 0"), + E::InvalidRecoveryController { address } => write!( + f, + "recovery: controller `{address}` is not a C-address strkey" + ), + E::InvalidBaselineDocHash { doc_hash } => write!( + f, + "recovery: baseline doc-hash `{doc_hash}` is not 64 lowercase hex characters" + ), + E::EmptyGuardianSet => write!(f, "recovery: guardian set is empty"), + E::DuplicateGuardian { address } => { + write!(f, "recovery: duplicate guardian `{address}`") + } + E::InvalidGuardianAddress { address } => write!( + f, + "recovery: guardian `{address}` is not a C- or G-address strkey" + ), + E::InvalidGuardianQuorum { quorum, n } => write!( + f, + "recovery: guardian quorum={quorum} out of range 1..={n} (guardian count)" + ), + E::InvalidRecoveryVerifier { address } => write!( + f, + "recovery: verifier `{address}` is not a C-address strkey" + ), + E::InvalidCircuitId { circuit_id } => write!( + f, + "recovery: circuit-id `{circuit_id}` is not valid non-empty hex" + ), + E::InvalidRecoveryPool { address } => { + write!(f, "recovery: pool `{address}` is not a C-address strkey") + } } } } @@ -529,6 +654,10 @@ pub fn validate(doc: &PolicyDoc) -> Result<(), Vec> { validate_rule(rule, &declared, &mut errors); } + if let Some(recovery) = &doc.recovery { + validate_recovery(recovery, &declared, &mut errors); + } + if errors.is_empty() { Ok(()) } else { @@ -721,3 +850,113 @@ fn validate_rule(rule: &Rule, declared: &BTreeSet<&str>, errors: &mut Vec bool { + s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) +} + +/// Validate a document's recovery configuration, if any. +fn validate_recovery( + r: &RecoveryConfig, + declared: &BTreeSet<&str>, + errors: &mut Vec, +) { + if r.replaceable.is_empty() { + errors.push(ValidationError::EmptyRecoveryReplaceable); + } + let mut seen_replaceable: BTreeSet<&str> = BTreeSet::new(); + for id in &r.replaceable { + if !seen_replaceable.insert(id) { + errors.push(ValidationError::DuplicateRecoveryReplaceable { id: id.clone() }); + } + if !declared.contains(id.as_str()) { + errors.push(ValidationError::UnknownRecoveryReplaceableRef { id: id.clone() }); + } + } + + if r.delay_ledgers == 0 { + errors.push(ValidationError::ZeroRecoveryDelayLedgers); + } + if r.expiry_ledgers == 0 { + errors.push(ValidationError::ZeroRecoveryExpiryLedgers); + } + if r.max_cancels == 0 { + errors.push(ValidationError::ZeroRecoveryMaxCancels); + } + + if !is_contract_address_shape(&r.controller) { + errors.push(ValidationError::InvalidRecoveryController { + address: r.controller.clone(), + }); + } + + if let Some(baseline) = &r.baseline { + if !is_sha256_hex(&baseline.doc_hash) { + errors.push(ValidationError::InvalidBaselineDocHash { + doc_hash: baseline.doc_hash.clone(), + }); + } + } + + match &r.mode { + RecoveryMode::GuardianOnly(g) => validate_guardian_set(g, errors), + RecoveryMode::ZkOnly(z) => validate_zk_verifier_config(z, errors), + RecoveryMode::Combined(g, z) => { + validate_guardian_set(g, errors); + validate_zk_verifier_config(z, errors); + } + } +} + +/// Shared checks for a [`GuardianSet`]: non-empty, no repeated address, every +/// address address-shaped, and `1 <= quorum <= guardians.len()` (mirroring +/// [`Principals::Threshold`]'s INV-1 rationale — a zero quorum authorizes +/// with no approvals; a quorum above the guardian count could never be met). +fn validate_guardian_set(g: &GuardianSet, errors: &mut Vec) { + if g.guardians.is_empty() { + errors.push(ValidationError::EmptyGuardianSet); + } + let mut seen: BTreeSet<&str> = BTreeSet::new(); + for addr in &g.guardians { + if !seen.insert(addr) { + errors.push(ValidationError::DuplicateGuardian { + address: addr.clone(), + }); + } + if !is_address_shape(addr) { + errors.push(ValidationError::InvalidGuardianAddress { + address: addr.clone(), + }); + } + } + if g.quorum == 0 || (g.quorum as usize) > g.guardians.len() { + errors.push(ValidationError::InvalidGuardianQuorum { + quorum: g.quorum, + n: g.guardians.len() as u32, + }); + } +} + +/// Shared checks for a [`ZkVerifierConfig`]: verifier and pool (if any) are +/// C-address shaped, and `circuit_id` is non-empty valid hex. +fn validate_zk_verifier_config(z: &ZkVerifierConfig, errors: &mut Vec) { + if !is_contract_address_shape(&z.verifier) { + errors.push(ValidationError::InvalidRecoveryVerifier { + address: z.verifier.clone(), + }); + } + if z.circuit_id.is_empty() || hex::decode(&z.circuit_id).is_err() { + errors.push(ValidationError::InvalidCircuitId { + circuit_id: z.circuit_id.clone(), + }); + } + if let Some(pool) = &z.pool { + if !is_contract_address_shape(pool) { + errors.push(ValidationError::InvalidRecoveryPool { + address: pool.clone(), + }); + } + } +} diff --git a/crates/perch-ir/tests/common/mod.rs b/crates/perch-ir/tests/common/mod.rs index 9f82271..a6cdebe 100644 --- a/crates/perch-ir/tests/common/mod.rs +++ b/crates/perch-ir/tests/common/mod.rs @@ -84,6 +84,7 @@ pub fn base_doc() -> PolicyDoc { network: None, signers: vec![signer("admin", WEBAUTHN_VERIFIER, ADMIN_KEY_HEX)], rules: vec![rule("admin", Scope::self_admin(), &["admin"])], + recovery: None, } } diff --git a/crates/perch-ir/tests/recovery.rs b/crates/perch-ir/tests/recovery.rs new file mode 100644 index 0000000..d1c019d --- /dev/null +++ b/crates/perch-ir/tests/recovery.rs @@ -0,0 +1,332 @@ +//! Recovery-configuration schema tests: the `ci-publish-recovery{,-combined}` +//! golden fixtures (canonical form + doc_hash, mirrored in +//! `packages/perch-js/test/parity.test.ts`), and semantic validation of +//! [`perch_ir::RecoveryConfig`]. See `CANONICAL.md` and `docs/recovery/`. + +mod common; + +use common::{assert_accepts, assert_rejects, base_doc}; +use perch_ir::{ + canonical_json, doc_hash_hex, from_json, validate, BaselineCommitment, GuardianSet, + PendingActivityPolicy, RecoveryConfig, RecoveryMode, RecoveryProfile, ValidationError, + ZkVerifierConfig, +}; +use std::fs; +use std::path::PathBuf; + +fn testdata(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../testdata") + .join(name) +} + +fn read(name: &str) -> String { + fs::read_to_string(testdata(name)).unwrap_or_else(|e| panic!("reading testdata/{name}: {e}")) +} + +const RECOVERY_HASH_HEX: &str = "dcd539d241eddba9537237f2958a639cd84ac9f1ed6111c18da4230b1b60b08d"; +const RECOVERY_COMBINED_HASH_HEX: &str = + "9a6c29fdfd28746f8826a945611f31f43e6a51d131ff0bfd018b1ce2762e24bb"; + +#[test] +fn guardian_only_fixture_parses_validates_and_matches_committed_files() { + let doc = from_json(&read("ci-publish-recovery.json")).expect("fixture must parse"); + validate(&doc).expect("fixture must validate"); + + let recovery = doc.recovery.as_ref().expect("fixture must enroll recovery"); + assert_eq!(recovery.profile, RecoveryProfile::Protected); + assert!(matches!(recovery.mode, RecoveryMode::GuardianOnly(_))); + assert_eq!(recovery.pending_activity, PendingActivityPolicy::Freeze); + assert!(recovery.baseline.is_some()); + assert_eq!(recovery.replaceable, ["admin".to_string()]); + + let committed = read("ci-publish-recovery.canonical.json"); + assert_eq!(canonical_json(&doc), committed.trim_end_matches('\n')); + + let hash = doc_hash_hex(&doc); + assert_eq!(hash, read("ci-publish-recovery.doc-hash").trim()); + assert_eq!(hash, RECOVERY_HASH_HEX); + + // Round-trip: the canonical form parses back to the same document. + let reparsed = from_json(&canonical_json(&doc)).expect("canonical form must parse"); + assert_eq!(doc, reparsed); +} + +#[test] +fn combined_mode_fixture_parses_validates_and_matches_committed_files() { + let doc = from_json(&read("ci-publish-recovery-combined.json")).expect("fixture must parse"); + validate(&doc).expect("fixture must validate"); + + let recovery = doc.recovery.as_ref().expect("fixture must enroll recovery"); + assert_eq!(recovery.profile, RecoveryProfile::Loss); + assert!(matches!(recovery.mode, RecoveryMode::Combined(_, _))); + assert_eq!(recovery.pending_activity, PendingActivityPolicy::Continue); + // Loss profile with no baseline: lost-key recovery only. + assert!(recovery.baseline.is_none()); + + let committed = read("ci-publish-recovery-combined.canonical.json"); + assert_eq!(canonical_json(&doc), committed.trim_end_matches('\n')); + + let hash = doc_hash_hex(&doc); + assert_eq!(hash, read("ci-publish-recovery-combined.doc-hash").trim()); + assert_eq!(hash, RECOVERY_COMBINED_HASH_HEX); +} + +/// A minimal, valid guardian-only recovery config referencing `base_doc()`'s +/// single "admin" signer. Test builder, not the schema's own default (there +/// is none — every field is explicit, per `pending_activity`'s no-default +/// requirement extended to the whole struct for test clarity). +fn guardian_only_recovery() -> RecoveryConfig { + RecoveryConfig { + profile: RecoveryProfile::Loss, + mode: RecoveryMode::GuardianOnly(GuardianSet { + guardians: vec![ + "GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH".into(), + "GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR".into(), + ], + quorum: 1, + }), + controller: "CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF".into(), + baseline: None, + replaceable: vec!["admin".into()], + delay_ledgers: 100, + expiry_ledgers: 1000, + max_cancels: 3, + pending_activity: PendingActivityPolicy::Continue, + } +} + +#[test] +fn accepts_minimal_guardian_only_recovery() { + let mut doc = base_doc(); + doc.recovery = Some(guardian_only_recovery()); + assert_accepts(&doc); +} + +#[test] +fn accepts_zk_only_recovery_with_no_guardian_field_at_all() { + // Guardian-only requires no ZK machinery; symmetrically, zk-only carries + // no guardian field to leave unset. + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::ZkOnly(ZkVerifierConfig { + verifier: "CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ".into(), + circuit_id: "21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb".into(), + pool: None, + }); + doc.recovery = Some(r); + assert_accepts(&doc); +} + +#[test] +fn accepts_baseline_pointing_at_a_different_documents_hash() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.baseline = Some(BaselineCommitment { + doc_hash: "27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98".into(), + }); + doc.recovery = Some(r); + assert_accepts(&doc); +} + +#[test] +fn rejects_empty_replaceable() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.replaceable = vec![]; + doc.recovery = Some(r); + assert_rejects(&doc, &ValidationError::EmptyRecoveryReplaceable); +} + +#[test] +fn rejects_duplicate_replaceable() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.replaceable = vec!["admin".into(), "admin".into()]; + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::DuplicateRecoveryReplaceable { id: "admin".into() }, + ); +} + +#[test] +fn rejects_replaceable_referencing_undeclared_signer() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.replaceable = vec!["nobody".into()]; + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::UnknownRecoveryReplaceableRef { + id: "nobody".into(), + }, + ); +} + +#[test] +fn rejects_zero_delay_expiry_and_max_cancels() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.delay_ledgers = 0; + doc.recovery = Some(r); + assert_rejects(&doc, &ValidationError::ZeroRecoveryDelayLedgers); + + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.expiry_ledgers = 0; + doc.recovery = Some(r); + assert_rejects(&doc, &ValidationError::ZeroRecoveryExpiryLedgers); + + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.max_cancels = 0; + doc.recovery = Some(r); + assert_rejects(&doc, &ValidationError::ZeroRecoveryMaxCancels); +} + +#[test] +fn rejects_malformed_controller_address() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.controller = "not-an-address".into(); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidRecoveryController { + address: "not-an-address".into(), + }, + ); +} + +#[test] +fn rejects_malformed_baseline_doc_hash() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.baseline = Some(BaselineCommitment { + doc_hash: "not-hex".into(), + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidBaselineDocHash { + doc_hash: "not-hex".into(), + }, + ); +} + +#[test] +fn rejects_empty_guardian_set() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::GuardianOnly(GuardianSet { + guardians: vec![], + quorum: 1, + }); + doc.recovery = Some(r); + assert_rejects(&doc, &ValidationError::EmptyGuardianSet); +} + +#[test] +fn rejects_duplicate_guardian() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + let addr = "GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH"; + r.mode = RecoveryMode::GuardianOnly(GuardianSet { + guardians: vec![addr.into(), addr.into()], + quorum: 1, + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::DuplicateGuardian { + address: addr.into(), + }, + ); +} + +#[test] +fn rejects_guardian_quorum_out_of_range() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::GuardianOnly(GuardianSet { + guardians: vec!["GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH".into()], + quorum: 0, + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidGuardianQuorum { quorum: 0, n: 1 }, + ); + + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::GuardianOnly(GuardianSet { + guardians: vec!["GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH".into()], + quorum: 2, + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidGuardianQuorum { quorum: 2, n: 1 }, + ); +} + +#[test] +fn rejects_malformed_zk_fields() { + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::ZkOnly(ZkVerifierConfig { + verifier: "not-an-address".into(), + circuit_id: "21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb".into(), + pool: None, + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidRecoveryVerifier { + address: "not-an-address".into(), + }, + ); + + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::ZkOnly(ZkVerifierConfig { + verifier: "CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ".into(), + circuit_id: "not-hex!".into(), + pool: None, + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidCircuitId { + circuit_id: "not-hex!".into(), + }, + ); + + let mut doc = base_doc(); + let mut r = guardian_only_recovery(); + r.mode = RecoveryMode::ZkOnly(ZkVerifierConfig { + verifier: "CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ".into(), + circuit_id: "21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb".into(), + pool: Some("not-an-address".into()), + }); + doc.recovery = Some(r); + assert_rejects( + &doc, + &ValidationError::InvalidRecoveryPool { + address: "not-an-address".into(), + }, + ); +} + +#[test] +fn recovery_absent_documents_hash_exactly_as_before_this_field_existed() { + // The load-bearing regression: base_doc() (no `recovery`) must canonicalize + // with no "recovery" key at all, and its hash must be unaffected — this is + // the same document every other perch-ir test already exercises, so this + // just makes the guarantee explicit and named. + let doc = base_doc(); + let canon = canonical_json(&doc); + assert!(!canon.contains("recovery"), "{canon}"); +} diff --git a/crates/perch-recovery/Cargo.toml b/crates/perch-recovery/Cargo.toml new file mode 100644 index 0000000..e9171b3 --- /dev/null +++ b/crates/perch-recovery/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "perch-recovery" +description = "Deployable account-recovery controller: guardian/ZK/combined recovery modes as an OZ smart-account Policy" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +# No cargo-manifest `publish = false`: release-plz's `release` command skips +# manifest-unpublishable packages (no tag ever). crates.io publish is blocked +# workspace-wide in release-plz.toml instead. + +[package.metadata.stellar] +cargo_inherit = true + +[lib] +crate-type = ["lib", "cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } +soroban-sdk-tools = { workspace = true, optional = true } +stellar-accounts = { workspace = true, optional = true } +perch-doc-compiler = { version = "0.2.1", path = "../perch-doc-compiler", default-features = false } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } +perch-testkit = { path = "../perch-testkit" } +perch-ir = { path = "../perch-ir" } + +[features] +# The deployable controller (storage, ZK adapter, `Policy` impl, entry +# points). Consumers that only cross-call an *adopted* instance (i.e. +# perch-smart-account) disable this so their wasm links no controller state +# or lifecycle code — mirrors perch-doc-compiler's `contract` feature split. +default = ["contract"] +contract = ["dep:soroban-sdk-tools", "dep:stellar-accounts"] diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs new file mode 100644 index 0000000..002c28b --- /dev/null +++ b/crates/perch-recovery/src/contract.rs @@ -0,0 +1,641 @@ +//! The recovery controller: an OZ smart-account [`Policy`] plus the +//! initiation/evidence/cancellation/reconfigure entry points. See +//! `docs/recovery/controller-governance.md` for the full design. +//! +//! **Why the security gate is not in `install`/`uninstall`:** OZ's +//! `remove_context_rule` calls a policy's `uninstall` via `try_uninstall` and +//! discards the result even if it panics (`stellar_accounts::smart_account:: +//! storage::remove_context_rule` — confirmed by reading the pinned dependency +//! directly), so a policy cannot rely on `uninstall` to block its own +//! removal. The real gate is [`guard_apply_doc`], called by +//! `perch-smart-account`'s `apply_doc` **before** any context rule is +//! touched — see that crate for the call site. `install`/`uninstall` here are +//! therefore plain bookkeeping, not authorization. + +use crate::storage::RecoveryStorage; +use crate::types::{Action, Attempt, AttemptState}; +use crate::zk::{self, ZkVerifierClient}; +use crate::{ReconfigureEvidence, RecoveryError}; +use perch_doc_compiler::{ + CompiledGuardianSet, CompiledRecoveryConfig, CompiledRecoveryMode, CompiledZkVerifierConfig, + RecoveryProfile, +}; +use soroban_sdk::auth::{Context, ContractContext}; +use soroban_sdk::xdr::ToXdr; +use soroban_sdk::{ + contract, contractevent, contractimpl, panic_with_error, Address, Bytes, BytesN, Env, IntoVal, + Symbol, TryFromVal, Vec, +}; +use stellar_accounts::policies::Policy; +use stellar_accounts::smart_account::{ContextRule, ContextRuleType, Signer}; + +/// Emitted when an attempt completes and installs its target document. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecoveryCompleted { + #[topic] + pub account: Address, + pub attempt_id: u64, + pub target_doc_hash: BytesN<32>, +} + +/// Emitted when an attempt is cancelled. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecoveryCancelled { + #[topic] + pub account: Address, + pub attempt_id: u64, +} + +/// Emitted when an attempt's evidence is satisfied and it becomes pending. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecoveryAuthorized { + #[topic] + pub account: Address, + pub attempt_id: u64, + pub executable_after: u32, + pub expires_at: u32, +} + +const TTL_THRESHOLD: u32 = 1; +const TTL_EXTEND: u32 = 3_110_400; // ~180 days at 5s ledgers; an operational tuning knob, not a security parameter. + +#[contract] +pub struct PerchRecovery; + +#[contractimpl] +impl Policy for PerchRecovery { + type AccountParams = CompiledRecoveryConfig; + + /// Write the enrolled configuration. The security gate already ran (see + /// module docs) — this only shape-checks the rule it's attached to and + /// stores the value. + fn install( + e: &Env, + install_params: CompiledRecoveryConfig, + context_rule: ContextRule, + smart_account: Address, + ) { + assert_self_zero_signer_rule(e, &context_rule, &smart_account); + RecoveryStorage::set_config(e, &smart_account, &install_params); + RecoveryStorage::extend_config_ttl(e, &smart_account, TTL_THRESHOLD, TTL_EXTEND); + } + + /// Variant A completion: authorize `apply_doc` exactly when a live, + /// authorized, unexpired attempt targets exactly the document being + /// applied. Consumes the attempt atomically as a side effect of + /// authorization succeeding — `perch-smart-account`'s own + /// `guard_apply_doc` pre-check relies on `has_pending` already reading + /// `false` by the time it runs, which only holds because this mutation + /// happens here, during auth evaluation, before `apply_doc`'s body runs. + fn enforce( + e: &Env, + context: Context, + _authenticated_signers: Vec, + _context_rule: ContextRule, + smart_account: Address, + ) { + complete(e, &context, &smart_account); + } + + /// Deliberately a no-op — see module docs on why this cannot be the + /// security gate. + fn uninstall(_e: &Env, _context_rule: ContextRule, _smart_account: Address) {} +} + +fn assert_self_zero_signer_rule(e: &Env, rule: &ContextRule, smart_account: &Address) { + if !rule.signers.is_empty() + || rule.context_type != ContextRuleType::CallContract(smart_account.clone()) + { + panic_with_error!(e, RecoveryError::MalformedContextRule); + } +} + +fn complete(e: &Env, context: &Context, smart_account: &Address) { + let Context::Contract(ContractContext { + contract, + fn_name, + args, + }) = context + else { + panic_with_error!(e, RecoveryError::WrongTarget); + }; + if contract != smart_account || *fn_name != Symbol::new(e, "apply_doc") { + panic_with_error!(e, RecoveryError::WrongTarget); + } + let Some(doc_json_val) = args.first() else { + panic_with_error!(e, RecoveryError::WrongTarget); + }; + let Ok(doc_json) = Bytes::try_from_val(e, &doc_json_val) else { + panic_with_error!(e, RecoveryError::WrongTarget); + }; + let doc_hash: BytesN<32> = e.crypto().sha256(&doc_json).to_bytes(); + + let mut attempt = RecoveryStorage::get_attempt(e, smart_account) + .unwrap_or_else(|| panic_with_error!(e, RecoveryError::NoLiveAttempt)); + if attempt.state != AttemptState::AuthorizedPending { + panic_with_error!(e, RecoveryError::AttemptNotAuthorized); + } + let now = e.ledger().sequence(); + if now < attempt.executable_after { + panic_with_error!(e, RecoveryError::AttemptNotExecutableYet); + } + if now >= attempt.expires_at { + panic_with_error!(e, RecoveryError::AttemptExpired); + } + if doc_hash != attempt.target_doc_hash { + panic_with_error!(e, RecoveryError::WrongTarget); + } + + // Atomic: mark completed, revoke every replaced credential permanently, + // spend the nullifier (if any). If installing the document itself later + // fails, this whole invocation — including this mutation — reverts with + // it (Soroban transaction atomicity), so there is no path where the + // attempt is consumed without the document actually installing. + attempt.state = AttemptState::Completed; + let mut revoked = RecoveryStorage::get_revoked(e, smart_account).unwrap_or_else(|| Vec::new(e)); + for c in attempt.replaced_credentials.iter() { + if !revoked.contains(&c) { + revoked.push_back(c); + } + } + RecoveryStorage::set_revoked(e, smart_account, &revoked); + RecoveryStorage::extend_revoked_ttl(e, smart_account, TTL_THRESHOLD, TTL_EXTEND); + if let Some(n) = attempt.nullifier.first() { + RecoveryStorage::set_nullifier(e, &n, &true); + RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, TTL_EXTEND); + } + RecoveryCompleted { + account: smart_account.clone(), + attempt_id: attempt.id, + target_doc_hash: attempt.target_doc_hash.clone(), + } + .publish(e); + RecoveryStorage::set_attempt(e, smart_account, &attempt); +} + +#[contractimpl] +impl PerchRecovery { + /// Called by `perch-smart-account`'s `apply_doc`, before any context rule + /// is touched, whenever a recovery controller is currently enrolled for + /// `account`. See module docs for why this — not `install`/`uninstall` — + /// is the actual security gate. + /// + /// Blocks unconditionally while a live attempt exists (property 9, + /// independent of §7 — see `docs/recovery/section-7-gate.md`). This also + /// reads `false` for a legitimate completion call, because `enforce` + /// already consumed the attempt during auth evaluation, before this runs. + /// + /// Otherwise: no change is always fine; a first enrollment (`old` is + /// `None`) is always fine; a change while `old.profile == Loss` is always + /// fine (§2.1); a change while `old.profile == Protected` — including + /// removing recovery — requires `evidence` to satisfy the *currently* + /// enrolled condition over a digest binding this exact transition. This + /// generalizes the validated experiment's strictly-additive-only + /// reconfigure (which remains reachable as the common case) to the full + /// requirement in the authoritative decision record §2 — see + /// `docs/recovery/controller-governance.md`. + pub fn guard_apply_doc( + e: &Env, + account: Address, + new_recovery: Vec, + evidence: ReconfigureEvidence, + ) -> Result<(), RecoveryError> { + if let Some(attempt) = RecoveryStorage::get_attempt(e, &account) { + if is_live(e, &attempt) { + return Err(RecoveryError::AttemptPending); + } + } + + let old = RecoveryStorage::get_config(e, &account); + let new = new_recovery.first(); + + if config_matches(&old, new.as_ref()) { + return Ok(()); + } + let Some(old_cfg) = old else { + return Ok(()); // first enrollment + }; + if old_cfg.profile == RecoveryProfile::Loss { + return Ok(()); + } + + // Protected: require the current condition's evidence over a digest + // binding (old config, proposed new config-or-removal). + let old_hash = config_hash_of(e, &old_cfg); + let new_hash = new.as_ref().map(|c| config_hash_of(e, c)); + let digest = zk::statement( + e, + &account, + &e.current_contract_address(), + &Action::Reconfigure, + &old_hash, + new_hash.as_ref(), + 0, + 0, + ); + + let guardians = guardian_set(&old_cfg.mode); + let zk_cfg = zk_config(&old_cfg.mode); + + if let Some(g) = guardians { + require_guardian_quorum(e, g, &evidence.guardians, &digest)?; + } + if let Some(z) = zk_cfg { + require_zk_evidence(e, z, &evidence, &digest)?; + } + if guardians.is_none() && zk_cfg.is_none() { + // Unreachable given RecoveryMode's own shape (every variant has a + // guardian factor, a ZK factor, or both) — defense in depth. + return Err(RecoveryError::ReconfigureEvidenceRequired); + } + Ok(()) + } + + /// Declare intent to restore access after key loss: target is the + /// current approved document with `replaced_credentials` replaced. + /// Permissionless — declaring intent carries no authority; the mode's + /// evidence is what authorizes anything. Refuses a live existing + /// attempt; replaces (releasing its nullifier) a terminal or expired one. + pub fn begin_lost_key_attempt( + e: &Env, + account: Address, + target_doc_hash: BytesN<32>, + replaced_credentials: Vec>, + ) -> Result { + begin_attempt( + e, + account, + Action::LostKey, + target_doc_hash, + replaced_credentials, + ) + } + + /// Declare intent to restore the enrolled baseline after suspected + /// compromise, with `replaced_credentials` replaced. Requires a baseline + /// to be enrolled. See `begin_lost_key_attempt` for the shared mechanics. + pub fn begin_compromise_attempt( + e: &Env, + account: Address, + target_doc_hash: BytesN<32>, + replaced_credentials: Vec>, + ) -> Result { + let config = require_config(e, &account)?; + if config.baseline.is_empty() { + return Err(RecoveryError::NoBaselineEnrolled); + } + begin_attempt( + e, + account, + Action::Compromise, + target_doc_hash, + replaced_credentials, + ) + } + + /// A guardian approves this account's pending attempt's initiation. + pub fn submit_guardian_approval( + e: &Env, + account: Address, + guardian: Address, + ) -> Result<(), RecoveryError> { + guardian.require_auth(); + let config = require_config(e, &account)?; + let g = guardian_set(&config.mode).ok_or(RecoveryError::ModeHasNoGuardians)?; + if !g.guardians.contains(&guardian) { + return Err(RecoveryError::NotAGuardian); + } + let mut attempt = require_attempt(e, &account)?; + if attempt.state != AttemptState::CollectingEvidence { + return Err(RecoveryError::AttemptNotAuthorized); + } + if attempt.guardian_approvals.contains(&guardian) { + return Err(RecoveryError::AlreadyApproved); + } + attempt.guardian_approvals.push_back(guardian); + maybe_promote(e, &config, &mut attempt); + RecoveryStorage::set_attempt(e, &account, &attempt); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + Ok(()) + } + + /// Submit a ZK initiation proof. Permissionless — the proof is the + /// authorization. `nullifier` is the prover-revealed nullifier; the + /// controller recomputes the statement itself from the attempt's own + /// frozen commitment, never trusting a caller-supplied statement. + pub fn submit_zk_proof( + e: &Env, + account: Address, + nullifier: BytesN<32>, + proof: Bytes, + ) -> Result<(), RecoveryError> { + let config = require_config(e, &account)?; + let z = zk_config(&config.mode).ok_or(RecoveryError::ModeHasNoZk)?; + let mut attempt = require_attempt(e, &account)?; + if attempt.state != AttemptState::CollectingEvidence { + return Err(RecoveryError::AttemptNotAuthorized); + } + if attempt.zk_verified { + return Ok(()); // idempotent re-submission + } + if RecoveryStorage::get_nullifier(e, &nullifier).unwrap_or(false) { + return Err(RecoveryError::NullifierAlreadySpent); + } + let cfg_hash = config_hash_of(e, &config); + let stmt = zk::statement( + e, + &account, + &e.current_contract_address(), + &attempt.action, + &cfg_hash, + Some(&attempt.target_doc_hash), + attempt.id, + config.delay_ledgers, + ); + if !ZkVerifierClient::new(e, &z.verifier).verify_proof(&stmt, &nullifier, &proof, &z.pool) { + return Err(RecoveryError::ZkProofInvalid); + } + attempt.zk_verified = true; + let mut nul = Vec::new(e); + nul.push_back(nullifier); + attempt.nullifier = nul; + maybe_promote(e, &config, &mut attempt); + RecoveryStorage::set_attempt(e, &account, &attempt); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + Ok(()) + } + + /// A guardian approves cancellation of this account's identified attempt. + /// Own action domain — initiation approvals never count here (§2.2). + pub fn submit_guardian_cancel( + e: &Env, + account: Address, + guardian: Address, + ) -> Result<(), RecoveryError> { + guardian.require_auth(); + let config = require_config(e, &account)?; + let g = guardian_set(&config.mode).ok_or(RecoveryError::ModeHasNoGuardians)?; + if !g.guardians.contains(&guardian) { + return Err(RecoveryError::NotAGuardian); + } + let mut attempt = require_attempt(e, &account)?; + let key = (account.clone(), attempt.id); + let mut tally = RecoveryStorage::get_cancel_tally(e, &key).unwrap_or_else(|| Vec::new(e)); + if tally.contains(&guardian) { + return Err(RecoveryError::AlreadyApproved); + } + tally.push_back(guardian); + if tally.len() >= g.quorum { + cancel_attempt(e, &account, &mut attempt)?; + } else { + RecoveryStorage::set_cancel_tally(e, &key, &tally); + RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + } + Ok(()) + } + + /// Submit a ZK cancellation proof for this account's identified attempt. + /// Own action domain (`Action::Cancel`) — an initiation proof never + /// satisfies this, and vice versa. + pub fn submit_zk_cancel( + e: &Env, + account: Address, + nullifier: BytesN<32>, + proof: Bytes, + ) -> Result<(), RecoveryError> { + let config = require_config(e, &account)?; + let z = zk_config(&config.mode).ok_or(RecoveryError::ModeHasNoZk)?; + let mut attempt = require_attempt(e, &account)?; + if RecoveryStorage::get_nullifier(e, &nullifier).unwrap_or(false) { + return Err(RecoveryError::NullifierAlreadySpent); + } + let cfg_hash = config_hash_of(e, &config); + let stmt = zk::statement( + e, + &account, + &e.current_contract_address(), + &Action::Cancel, + &cfg_hash, + None, + attempt.id, + config.delay_ledgers, + ); + if !ZkVerifierClient::new(e, &z.verifier).verify_proof(&stmt, &nullifier, &proof, &z.pool) { + return Err(RecoveryError::ZkProofInvalid); + } + cancel_attempt(e, &account, &mut attempt)?; + RecoveryStorage::set_nullifier(e, &nullifier, &true); + Ok(()) + } + + /// The enrolled configuration, if any. + pub fn get_config(e: &Env, account: Address) -> Option { + RecoveryStorage::get_config(e, &account) + } + + /// `sha256(to_xdr(config))` of the enrolled configuration, if any — the + /// scoped commitment a proof's statement binds, distinct from the whole + /// document's own `doc_hash` (which already covers this configuration + /// too — see `docs/recovery/schema.md`). + pub fn config_hash(e: &Env, account: Address) -> Option> { + RecoveryStorage::get_config(e, &account).map(|c| config_hash_of(e, &c)) + } + + /// The account's current attempt, if any (live or terminal — callers + /// checking liveness should also consult [`Self::has_pending`]). + pub fn get_attempt(e: &Env, account: Address) -> Option { + RecoveryStorage::get_attempt(e, &account) + } + + /// Whether a live (not completed/cancelled, not expired) attempt exists. + pub fn has_pending(e: &Env, account: Address) -> bool { + RecoveryStorage::get_attempt(e, &account).is_some_and(|a| is_live(e, &a)) + } +} + +fn require_config(e: &Env, account: &Address) -> Result { + RecoveryStorage::get_config(e, account).ok_or(RecoveryError::NotEnrolled) +} + +fn require_attempt(e: &Env, account: &Address) -> Result { + RecoveryStorage::get_attempt(e, account).ok_or(RecoveryError::NoLiveAttempt) +} + +/// Live = not terminal, and (if authorized) not past its expiry. A +/// `CollectingEvidence` attempt has no expiry of its own in this design (only +/// an authorized attempt's *completion window* expires) — it is live until +/// explicitly cancelled or replaced by a fresh `begin_*_attempt` call. +fn is_live(e: &Env, attempt: &Attempt) -> bool { + match attempt.state { + AttemptState::CollectingEvidence => true, + AttemptState::AuthorizedPending => e.ledger().sequence() < attempt.expires_at, + AttemptState::Completed | AttemptState::Cancelled => false, + } +} + +fn guardian_set(mode: &CompiledRecoveryMode) -> Option<&CompiledGuardianSet> { + match mode { + CompiledRecoveryMode::GuardianOnly(g) | CompiledRecoveryMode::Combined(g, _) => Some(g), + CompiledRecoveryMode::ZkOnly(_) => None, + } +} + +fn zk_config(mode: &CompiledRecoveryMode) -> Option<&CompiledZkVerifierConfig> { + match mode { + CompiledRecoveryMode::ZkOnly(z) | CompiledRecoveryMode::Combined(_, z) => Some(z), + CompiledRecoveryMode::GuardianOnly(_) => None, + } +} + +fn config_hash_of(e: &Env, cfg: &CompiledRecoveryConfig) -> BytesN<32> { + e.crypto().sha256(&cfg.clone().to_xdr(e)).to_bytes() +} + +fn config_matches( + old: &Option, + new: Option<&CompiledRecoveryConfig>, +) -> bool { + match (old, new) { + (None, None) => true, + (Some(a), Some(b)) => a == b, + _ => false, + } +} + +fn initiation_satisfied(mode: &CompiledRecoveryMode, attempt: &Attempt) -> bool { + match mode { + CompiledRecoveryMode::GuardianOnly(g) => attempt.guardian_approvals.len() >= g.quorum, + CompiledRecoveryMode::ZkOnly(_) => attempt.zk_verified, + CompiledRecoveryMode::Combined(g, _) => { + attempt.guardian_approvals.len() >= g.quorum && attempt.zk_verified + } + } +} + +fn maybe_promote(e: &Env, config: &CompiledRecoveryConfig, attempt: &mut Attempt) { + if attempt.state == AttemptState::CollectingEvidence + && initiation_satisfied(&config.mode, attempt) + { + let now = e.ledger().sequence(); + attempt.state = AttemptState::AuthorizedPending; + attempt.executable_after = now + config.delay_ledgers; + attempt.expires_at = attempt.executable_after + config.expiry_ledgers; + RecoveryAuthorized { + account: e.current_contract_address(), + attempt_id: attempt.id, + executable_after: attempt.executable_after, + expires_at: attempt.expires_at, + } + .publish(e); + } +} + +fn begin_attempt( + e: &Env, + account: Address, + action: Action, + target_doc_hash: BytesN<32>, + replaced_credentials: Vec>, +) -> Result { + let config = require_config(e, &account)?; + for c in replaced_credentials.iter() { + if !config.replaceable.contains(&c) { + return Err(RecoveryError::CredentialNotReplaceable); + } + } + if let Some(existing) = RecoveryStorage::get_attempt(e, &account) { + if is_live(e, &existing) { + return Err(RecoveryError::AttemptPending); + } + // Stale (terminal or expired): release its nullifier before + // replacing it, so the same secret can be used again. + if let Some(n) = existing.nullifier.first() { + RecoveryStorage::set_nullifier(e, &n, &false); + } + } + let id = RecoveryStorage::get_next_attempt_id(e, &account).unwrap_or(0); + RecoveryStorage::set_next_attempt_id(e, &account, &(id + 1)); + + let attempt = Attempt { + id, + action, + target_doc_hash, + replaced_credentials, + created_at: e.ledger().sequence(), + executable_after: 0, + expires_at: 0, + guardian_approvals: Vec::new(e), + zk_verified: false, + nullifier: Vec::new(e), + state: AttemptState::CollectingEvidence, + }; + RecoveryStorage::set_attempt(e, &account, &attempt); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + Ok(id) +} + +fn cancel_attempt(e: &Env, account: &Address, attempt: &mut Attempt) -> Result<(), RecoveryError> { + let used = RecoveryStorage::get_cancels_used(e, account).unwrap_or(0); + let config = require_config(e, account)?; + if used >= config.max_cancels { + return Err(RecoveryError::MaxCancelsReached); + } + RecoveryStorage::set_cancels_used(e, account, &(used + 1)); + attempt.state = AttemptState::Cancelled; + if let Some(n) = attempt.nullifier.first() { + RecoveryStorage::set_nullifier(e, &n, &false); + } + RecoveryStorage::set_attempt(e, account, attempt); + RecoveryCancelled { + account: account.clone(), + attempt_id: attempt.id, + } + .publish(e); + Ok(()) +} + +fn require_guardian_quorum( + e: &Env, + g: &CompiledGuardianSet, + submitted: &Vec
, + digest: &BytesN<32>, +) -> Result<(), RecoveryError> { + let mut counted = Vec::new(e); + for addr in submitted.iter() { + if !g.guardians.contains(&addr) || counted.contains(&addr) { + continue; + } + // The host aborts the whole transaction if `addr` did not actually + // authorize this exact digest — a claimed-but-unsigned address never + // silently passes. + addr.require_auth_for_args(Vec::from_array(e, [digest.into_val(e)])); + counted.push_back(addr); + } + if counted.len() < g.quorum { + return Err(RecoveryError::ReconfigureEvidenceRequired); + } + Ok(()) +} + +fn require_zk_evidence( + e: &Env, + z: &CompiledZkVerifierConfig, + evidence: &ReconfigureEvidence, + digest: &BytesN<32>, +) -> Result<(), RecoveryError> { + let (Some(nullifier), Some(proof)) = (evidence.zk_nullifier.first(), evidence.zk_proof.first()) + else { + return Err(RecoveryError::ReconfigureEvidenceRequired); + }; + if RecoveryStorage::get_nullifier(e, &nullifier).unwrap_or(false) { + return Err(RecoveryError::NullifierAlreadySpent); + } + if !ZkVerifierClient::new(e, &z.verifier).verify_proof(digest, &nullifier, &proof, &z.pool) { + return Err(RecoveryError::ZkProofInvalid); + } + RecoveryStorage::set_nullifier(e, &nullifier, &true); + Ok(()) +} diff --git a/crates/perch-recovery/src/lib.rs b/crates/perch-recovery/src/lib.rs new file mode 100644 index 0000000..35c4b72 --- /dev/null +++ b/crates/perch-recovery/src/lib.rs @@ -0,0 +1,116 @@ +//! `perch-recovery`: the shared account-recovery controller, generalizing the +//! validated Nido Stage 3 experiment into a perch crate. Guardian-only, +//! ZK-only, and combined recovery modes; strictly-additive-and-beyond +//! reconfigure under `Protected`; `config_hash` commitment; constructorless +//! deployment. See `docs/recovery/` for the full design, governance, and +//! open release-blocking gate (§7, pending-activity policy). +//! +//! Mirrors `perch-doc-compiler`'s split: the error type, the evidence type, +//! and a client-only cross-contract interface are always available; the +//! actual controller (storage, the ZK statement/adapter, the `Policy` impl +//! and entry points) sits behind the default `contract` feature, so +//! `perch-smart-account` links no controller logic into account wasm — only +//! the client it cross-calls the *adopted* instance through. +#![no_std] + +extern crate alloc; + +use perch_doc_compiler::CompiledRecoveryConfig; +use soroban_sdk::{contractclient, contracttype, Address, Bytes, BytesN, Env, Vec}; +use soroban_sdk_tools::scerr; + +/// Everything the controller's entry points can refuse. +#[scerr] +pub enum RecoveryError { + /// No `RecoveryConfig` is enrolled for this account. + NotEnrolled, + /// `apply_doc` was attempted while a live (not completed/cancelled, not + /// expired) attempt exists for this account. + AttemptPending, + /// No attempt exists, or it is terminal/expired. + NoLiveAttempt, + /// The attempt exists but its mode's evidence is not yet satisfied. + AttemptNotAuthorized, + /// An authorized attempt's timelock has not elapsed yet. + AttemptNotExecutableYet, + /// An authorized attempt has lapsed past its expiry. + AttemptExpired, + /// The document being applied does not hash to the attempt's committed + /// target. + WrongTarget, + /// The same guardian already approved this evidence. + AlreadyApproved, + /// The address is not a member of the enrolled guardian set. + NotAGuardian, + /// The enrolled mode has no guardian factor at all. + ModeHasNoGuardians, + /// The enrolled mode has no ZK factor at all. + ModeHasNoZk, + /// The account's lifetime cancellation cap has been reached. + MaxCancelsReached, + /// A declared replaceable credential is not in the enrolled + /// configuration's `replaceable` set. + CredentialNotReplaceable, + /// The nullifier has already been spent (by this account or another — + /// nullifier uniqueness is global by design). + NullifierAlreadySpent, + /// The verifier rejected the proof. + ZkProofInvalid, + /// A `Protected` reconfiguration (including disabling recovery) was + /// attempted without the currently-enrolled condition's evidence. + ReconfigureEvidenceRequired, + /// Suspected-compromise recovery was attempted with no baseline enrolled. + NoBaselineEnrolled, + /// A context rule attached this policy with the wrong shape (non-empty + /// signers, or not scoped to the smart account calling itself). + MalformedContextRule, +} + +/// Evidence accompanying an `apply_doc` call that changes a `Protected` +/// recovery configuration (including removing it). Ignored when no change is +/// being made, or the currently-enrolled profile is `Loss`. See +/// `docs/recovery/controller-governance.md`. +#[contracttype] +#[derive(Clone, Debug)] +pub struct ReconfigureEvidence { + /// A quorum-sized subset of the currently-enrolled guardian set, each of + /// which must independently authorize this exact call (the host rejects + /// the whole invocation if any named address didn't actually sign). + pub guardians: Vec
, + /// Zero or one `(nullifier, proof)` pair, for a ZK-capable enrolled mode. + pub zk_nullifier: Vec>, + pub zk_proof: Vec, +} + +/// Cross-contract client, generated independently of the deployable (see +/// module docs) — a consumer that only needs to cross-call an *adopted* +/// controller instance links no storage, ZK-adapter, or `Policy`-lifecycle +/// code at all. Every entry point `perch-smart-account`'s `apply_doc` calls +/// lives here; anything else (initiation, evidence submission, cancellation, +/// read-only queries) is only reachable with the `contract` feature on, +/// since only a recovering party or guardian — never the account's own +/// wasm — calls those. +#[allow(unused)] +#[contractclient(name = "RecoveryControllerClient")] +trait RecoveryControllerClientInterface { + fn guard_apply_doc( + e: &Env, + account: Address, + new_recovery: Vec, + evidence: ReconfigureEvidence, + ) -> Result<(), RecoveryError>; +} + +#[cfg(feature = "contract")] +mod contract; +#[cfg(feature = "contract")] +mod storage; +#[cfg(feature = "contract")] +pub mod types; +#[cfg(feature = "contract")] +pub mod zk; + +#[cfg(feature = "contract")] +pub use contract::{ + PerchRecovery, PerchRecoveryClient, RecoveryAuthorized, RecoveryCancelled, RecoveryCompleted, +}; diff --git a/crates/perch-recovery/src/storage.rs b/crates/perch-recovery/src/storage.rs new file mode 100644 index 0000000..572e3c1 --- /dev/null +++ b/crates/perch-recovery/src/storage.rs @@ -0,0 +1,38 @@ +//! Typed storage handles, via `soroban-sdk-tools`' `#[contractstorage]` (see +//! `crates/perch-smart-account/src/lib.rs` for the same convention). All +//! persistent — recovery state must outlive the enrolling account's own +//! `applied_doc` bumps, and TTL is extended explicitly wherever a write +//! matters (see each call site in `contract.rs`). + +use crate::types::Attempt; +use perch_doc_compiler::CompiledRecoveryConfig; +use soroban_sdk::{Address, BytesN, Vec}; +use soroban_sdk_tools::{contractstorage, PersistentMap}; + +#[contractstorage] +#[allow(dead_code)] // the field names only derive storage keys + accessors +pub struct RecoveryStorage { + /// Enrolled configuration per account. Written only by `install` (after + /// `guard_apply_doc` has already authorized the change — see + /// `contract.rs`'s module docs on why the gate can't live here). + pub config: PersistentMap, + /// The account's single live-or-terminal attempt, if any. + pub attempt: PersistentMap, + /// Monotonic per-account attempt-id counter — the proposal commitment's + /// nonce. Never reused, even across terminal attempts. + pub next_attempt_id: PersistentMap, + /// Cumulative cancellations across the account's whole history (§2.2's + /// griefing bound), never reset by a new attempt. + pub cancels_used: PersistentMap, + /// Guardian cancel-evidence for one `(account, attempt_id)` — a + /// deliberately separate domain from `Attempt::guardian_approvals` + /// (initiation evidence never counts toward cancellation, per §2.2). + pub cancel_tally: PersistentMap<(Address, u64), Vec
>, + /// Permanent, append-only fingerprints of every credential this account + /// has ever had recovery-revoked. A later attempt — even from an old + /// baseline — must never reintroduce one of these. + pub revoked: PersistentMap>>, + /// Global (not per-account) spent-nullifier set — a ZK nullifier's + /// uniqueness is cross-account by design. + pub nullifier: PersistentMap, bool>, +} diff --git a/crates/perch-recovery/src/types.rs b/crates/perch-recovery/src/types.rs new file mode 100644 index 0000000..c2f7cb1 --- /dev/null +++ b/crates/perch-recovery/src/types.rs @@ -0,0 +1,84 @@ +//! Storage types for the recovery controller. +//! +//! Enrolled configuration is stored as-is from the compiler's wire type +//! (`perch_doc_compiler::CompiledRecoveryConfig`) — no separate controller-side +//! config type exists, so there is exactly one place the shape is defined. + +use soroban_sdk::{contracttype, Address, BytesN, Vec}; + +/// Which action a proposal or evidence submission is for. Domain-separates +/// initiation from cancellation from reconfiguration (§2.2): evidence +/// collected for one action never satisfies another, even for the same +/// attempt or account. +#[derive(Clone, Debug, PartialEq, Eq)] +#[contracttype] +pub enum Action { + /// Restore access after key loss: target is the current document with + /// designated credentials replaced. + LostKey, + /// Restore an approved baseline after suspected compromise. + Compromise, + /// Cancel the identified attempt. + Cancel, + /// Change the enrolled recovery configuration itself. + Reconfigure, +} + +/// A recovery attempt's lifecycle state. `Expired` is deliberately not a +/// variant — it is derived from `expires_at` vs. the current ledger sequence +/// (see [`crate::contract::is_live`]), so no separate transition can ever +/// desync it from the stored timing fields. +#[derive(Clone, Debug, PartialEq, Eq)] +#[contracttype] +pub enum AttemptState { + /// Declared; waiting for its mode's evidence to be satisfied. + CollectingEvidence, + /// Evidence satisfied; waiting out the timelock delay, then completable + /// until it expires. + AuthorizedPending, + /// Consumed by a successful completion. + Completed, + /// Consumed by cancellation. + Cancelled, +} + +/// An account's single live-or-terminal recovery attempt. An account has at +/// most one `Attempt` in storage at a time; `begin_attempt` refuses to +/// replace a live one and replaces (releasing its nullifier) a terminal or +/// expired one. +#[derive(Clone, Debug, PartialEq, Eq)] +#[contracttype] +pub struct Attempt { + /// Monotonic per-account id — the proposal commitment's nonce. + pub id: u64, + pub action: Action, + /// `sha256(canonical bytes)` of the exact document this attempt, once + /// completed, installs. Frozen at `begin_attempt` and never recomputed — + /// completion checks the caller's `apply_doc` argument against exactly + /// this value. + pub target_doc_hash: BytesN<32>, + /// Credential fingerprints this attempt replaces (a subset of the + /// enrolled config's `replaceable`, chosen by the caller at initiation — + /// see `docs/recovery/`). Appended to the account's permanent revoked set + /// on completion. + pub replaced_credentials: Vec>, + /// Ledger sequence `begin_attempt` ran at. + pub created_at: u32, + /// Ledger sequence at or after which this attempt becomes completable, + /// once authorized. `0` (unset) while `CollectingEvidence`. + pub executable_after: u32, + /// Ledger sequence at or after which an authorized attempt lapses. + /// `0` (unset) while `CollectingEvidence`. + pub expires_at: u32, + /// Guardian addresses that have submitted approval for this attempt's + /// initiation. Cancellation evidence is tracked separately (its own + /// action domain — see [`crate::storage`]'s `cancel_tally`). + pub guardian_approvals: Vec
, + /// Whether a valid initiation proof has been submitted for this attempt. + pub zk_verified: bool, + /// Nullifier consumed by this attempt's ZK evidence, if any (zero or one + /// entry) — released back to unspent if the attempt is replaced before + /// promotion, spent permanently on completion. + pub nullifier: Vec>, + pub state: AttemptState, +} diff --git a/crates/perch-recovery/src/zk.rs b/crates/perch-recovery/src/zk.rs new file mode 100644 index 0000000..2de85aa --- /dev/null +++ b/crates/perch-recovery/src/zk.rs @@ -0,0 +1,89 @@ +//! Generic ZK verifier adapter: the statement a proof must bind, and the +//! cross-contract interface any verifier implementation satisfies. +//! +//! This module deliberately carries **no circuit**. Per the authoritative +//! decision record §5.4, proof-system portability is an interface goal, not +//! a requirement to ship or validate a specific circuit in this stage — see +//! `docs/recovery/controller-governance.md`'s "ZK adapter scope" section. +//! What this module fixes is the *statement* a proof must be over: every +//! recovery-specific public fact, bound the same way regardless of which +//! proof system a given verifier implements. + +use crate::types::Action; +use soroban_sdk::xdr::ToXdr; +use soroban_sdk::{Address, Bytes, BytesN, Env, Vec}; + +/// A domain-separation tag, so a statement can never be confused with a hash +/// computed for an unrelated purpose (`doc_hash`, `config_hash`, ...) even if +/// the byte lengths happened to coincide. +const STATEMENT_DOMAIN: &str = "perch-recovery-statement-v1"; + +fn action_tag(action: &Action) -> u8 { + match action { + Action::LostKey => 0, + Action::Compromise => 1, + Action::Cancel => 2, + Action::Reconfigure => 3, + } +} + +/// The statement a recovery proof must be over: network, account, and +/// controller identity; the action being authorized; the enrolled +/// configuration's own content hash (so a proof cannot outlive a +/// reconfiguration — see `contract.rs`'s reconfigure-evidence handling); the +/// target document's hash for `LostKey`/`Compromise` (zero for `Cancel`, +/// which identifies its attempt by id instead, per §2.2); the attempt's +/// nonce; and the enrolled timelock. Binding all of this in one hash means a +/// proof for one account/action/config/target/attempt can never be replayed +/// for another — every field the follow-up review §5.3 names for a proposal +/// commitment is present. +#[allow(clippy::too_many_arguments)] +pub fn statement( + e: &Env, + account: &Address, + controller: &Address, + action: &Action, + config_hash: &BytesN<32>, + target_doc_hash: Option<&BytesN<32>>, + attempt_id: u64, + delay_ledgers: u32, +) -> BytesN<32> { + let mut buf = Bytes::from_slice(e, STATEMENT_DOMAIN.as_bytes()); + buf.append(&Bytes::from_slice(e, &e.ledger().network_id().to_array())); + buf.append(&account.to_xdr(e)); + buf.append(&controller.to_xdr(e)); + buf.push_back(action_tag(action)); + buf.append(&Bytes::from_slice(e, &config_hash.to_array())); + match target_doc_hash { + Some(h) => buf.append(&Bytes::from_slice(e, &h.to_array())), + None => buf.append(&Bytes::from_slice(e, &[0u8; 32])), + } + buf.append(&Bytes::from_slice(e, &attempt_id.to_be_bytes())); + buf.append(&Bytes::from_slice(e, &delay_ledgers.to_be_bytes())); + e.crypto().sha256(&buf).to_bytes() +} + +/// Cross-contract interface a ZK verifier instance implements. Deliberately +/// minimal and proof-system-agnostic: the controller computes `statement` +/// itself from its own stored attempt/config (never trusting a +/// caller-supplied statement — see `contract.rs`'s `submit_zk_proof`), and +/// treats `verify_proof`'s result as the only fact it needs. +/// +/// `nullifier` is the prover-revealed nullifier the circuit derives from its +/// secret; the controller tracks spent nullifiers itself (globally, across +/// accounts — see `storage.rs`), so a verifier need not. `pool`, present only +/// for schemes that prove membership of a secret in a set, is opaque to the +/// controller: it is passed through unchanged from the enrolled +/// `ZkVerifierConfig.pool`, and interpreting it is entirely the verifier's +/// concern. +#[allow(unused)] +#[soroban_sdk::contractclient(name = "ZkVerifierClient")] +pub trait ZkVerifierInterface { + fn verify_proof( + e: &Env, + statement: BytesN<32>, + nullifier: BytesN<32>, + proof: Bytes, + pool: Vec
, + ) -> bool; +} diff --git a/crates/perch-smart-account/Cargo.toml b/crates/perch-smart-account/Cargo.toml index 8ad1066..1e80d6e 100644 --- a/crates/perch-smart-account/Cargo.toml +++ b/crates/perch-smart-account/Cargo.toml @@ -17,6 +17,10 @@ stellar-accounts = { workspace = true } # Client + wire types only — `contract` off, so no compiler code (parser, # lowering) is ever linked into account wasm. perch-doc-compiler = { version = "0.2.0", path = "../perch-doc-compiler", default-features = false } +# Client + types only for the recovery controller — apply_doc cross-calls the +# adopted instance; `contract` off, so no controller state or Policy-lifecycle +# code is linked into account wasm. +perch-recovery = { version = "0.1.0", path = "../perch-recovery", default-features = false } # The pure `registry_id + wasm_hash → address` derivation `apply_doc` uses to # resolve the compiler + interpreter from the pinned stateless-registry id. perch-registry-resolve = { version = "0.1.1", path = "../perch-registry-resolve" } diff --git a/crates/perch-smart-account/src/lib.rs b/crates/perch-smart-account/src/lib.rs index 5e09e49..384a612 100644 --- a/crates/perch-smart-account/src/lib.rs +++ b/crates/perch-smart-account/src/lib.rs @@ -23,6 +23,8 @@ use perch_doc_compiler::{ CompiledDoc, CompiledRule, DocCompilerClient, DocCompilerError, RuleScope, }; +pub use perch_recovery::ReconfigureEvidence; +use perch_recovery::{RecoveryControllerClient, RecoveryError}; use soroban_sdk::{ auth::CustomAccountInterface, contractevent, contracttrait, Address, Bytes, BytesN, Env, Map, String, Val, Vec, @@ -78,6 +80,8 @@ pub enum PerchAccountError { AdminLockout, #[from_contract_client] Compiler(DocCompilerError), + #[from_contract_client] + Recovery(RecoveryError), } // scerr's composed (root) mode predates sdk 27's spec-shaking marker; the @@ -98,6 +102,13 @@ pub struct DocApplied { struct PerchStorage { /// Canonical `doc_hash` of the currently applied policy document. applied_doc: InstanceItem>, + /// The recovery-controller instance currently adopted (the address named + /// by the applied document's `recovery.controller`, if any). `None` when + /// no recovery is enrolled. Read before the next `apply_doc`'s rule swap + /// to gate a change against the *currently* enrolled condition — see + /// [`apply_doc`](PerchSmartAccount::apply_doc) and + /// `docs/recovery/controller-governance.md`. + recovery_controller: InstanceItem
, } /// The doc-only smart account surface. Implementers get OZ evaluation from @@ -113,7 +124,16 @@ pub trait PerchSmartAccount: CustomAccountInterface + SmartAccount { /// cross-contract call), never passed in. Replaces the entire rule set /// atomically and returns the canonical `doc_hash`. Runs under the account's /// own authorization: the admin rule must approve the call. - fn apply_doc(e: &Env, doc_json: Bytes) -> Result, PerchAccountError> { + /// + /// `recovery_evidence` is consulted only when the document changes a + /// currently-`Protected` recovery configuration (including removing it); + /// pass an empty [`ReconfigureEvidence`] otherwise. See + /// `docs/recovery/controller-governance.md`. + fn apply_doc( + e: &Env, + doc_json: Bytes, + recovery_evidence: ReconfigureEvidence, + ) -> Result, PerchAccountError> { e.current_contract_address().require_auth(); // Resolve the infra offline: each `address(e)` derives @@ -131,6 +151,23 @@ pub trait PerchSmartAccount: CustomAccountInterface + SmartAccount { ensure_admin_survives(&compiled)?; + // Recovery gate: only reachable when a controller is CURRENTLY + // enrolled (zero overhead for the common non-recovery account) — + // blocks unconditionally while a live attempt exists, and requires + // `recovery_evidence` when the currently-enrolled profile is + // `Protected` and the configuration is changing. Must run before any + // context rule is touched: OZ's `remove_context_rule` cannot be + // trusted to enforce anything via a policy's `uninstall` (it swallows + // panics — see `perch-recovery`'s controller module docs), so this + // cross-call, not that lifecycle hook, is the actual gate. + if let Some(old_controller) = PerchStorage::get_recovery_controller(e) { + RecoveryControllerClient::new(e, &old_controller).try_guard_apply_doc( + &e.current_contract_address(), + &compiled.recovery, + &recovery_evidence, + )??; + } + // Replace the entire rule set. One invocation — all-or-nothing; // there is no observable half-migrated state. let next_id: u32 = e @@ -150,6 +187,30 @@ pub trait PerchSmartAccount: CustomAccountInterface + SmartAccount { install_rule(e, &interpreter, &rule); } + // The recovery rule is not one of `doc.rules` (recovery is top-level + // document configuration, not a rule — see `docs/recovery/schema.md`) + // and is installed directly here: a zero-signer `self-admin` rule + // whose sole attached policy is the adopted controller. `install()` + // receives the compiled config as install params; completion later + // authorizes `apply_doc` through this same rule's `enforce()` + // (Variant A) — see `docs/recovery/controller-governance.md`. + match compiled.recovery.first() { + Some(recovery) => { + let mut policies: Map = Map::new(e); + policies.set(recovery.controller.clone(), recovery.into_val(e)); + smart_account::add_context_rule( + e, + &ContextRuleType::CallContract(e.current_contract_address()), + &String::from_str(e, "recovery"), + None, + &Vec::new(e), + &policies, + ); + PerchStorage::set_recovery_controller(e, &recovery.controller); + } + None => PerchStorage::remove_recovery_controller(e), + } + PerchStorage::set_applied_doc(e, &compiled.doc_hash); DocApplied { doc_hash: compiled.doc_hash.clone(), @@ -262,6 +323,7 @@ macro_rules! impl_perch_smart_account { use $crate::soroban_sdk::auth::CustomAccountInterface; use $crate::stellar_accounts::smart_account::SmartAccount; use $crate::PerchSmartAccount; + use $crate::ReconfigureEvidence; #[$crate::soroban_sdk::contractimpl] impl CustomAccountInterface for $ty { diff --git a/crates/perch-testkit/Cargo.toml b/crates/perch-testkit/Cargo.toml index d06754a..8ff1dae 100644 --- a/crates/perch-testkit/Cargo.toml +++ b/crates/perch-testkit/Cargo.toml @@ -23,4 +23,5 @@ perch-spending-limit = { version = "0.1.1", path = "../perch-spending-limit" } # The pinned infra-resolution modules (`registry_pins`), so native mode registers # the infra at exactly the content addresses the account derives. perch-smart-account = { version = "0.1.1", path = "../perch-smart-account" } +perch-recovery = { version = "0.1.0", path = "../perch-recovery" } hex = "0.4" diff --git a/crates/perch-testkit/src/fixture.rs b/crates/perch-testkit/src/fixture.rs index 1032fff..3d27078 100644 --- a/crates/perch-testkit/src/fixture.rs +++ b/crates/perch-testkit/src/fixture.rs @@ -31,6 +31,18 @@ pub const FIXTURE_REGISTRY: &str = "CCA7QAA6OD6LQJTU2MKN6EAS5I52QIFPAYMMQYSU7KHW pub const CI_PUBLISH_DOC_HASH: &str = "27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98"; +/// Empty [`perch_recovery::ReconfigureEvidence`] — `apply_doc`'s second +/// argument for every call that isn't reconfiguring a `Protected` recovery +/// enrollment (the overwhelming majority of calls, including every account +/// that never enrolls recovery at all). +pub fn no_recovery_evidence(env: &Env) -> perch_recovery::ReconfigureEvidence { + perch_recovery::ReconfigureEvidence { + guardians: Vec::new(env), + zk_nullifier: Vec::new(env), + zk_proof: Vec::new(env), + } +} + /// The frozen ci-publish conformance fixture (CANON v1), read from /// `testdata/ci-publish.json` at the workspace root. pub fn fixture() -> String { diff --git a/crates/perch-testkit/src/lib.rs b/crates/perch-testkit/src/lib.rs index 8e3e171..a333536 100644 --- a/crates/perch-testkit/src/lib.rs +++ b/crates/perch-testkit/src/lib.rs @@ -14,7 +14,8 @@ //! .build(); //! //! let doc = soroban_sdk::Bytes::from_slice(&w.env, perch_testkit::fixture().as_bytes()); -//! let _hash = w.account_client().apply_doc(&doc, &w.compiler, &w.interpreter); +//! let evidence = perch_testkit::no_recovery_evidence(&w.env); +//! let _hash = w.account_client().apply_doc(&doc, &evidence); //! ``` //! //! # Modes @@ -40,8 +41,8 @@ pub mod manifest; pub mod native; pub use fixture::{ - auth_digest, ci_publish_doc_hash, fixture, AnyKeyVerifier, CI_PUBLISH_DOC_HASH, - FIXTURE_NETWORK, FIXTURE_REGISTRY, FIXTURE_VERIFIERS, + auth_digest, ci_publish_doc_hash, fixture, no_recovery_evidence, AnyKeyVerifier, + CI_PUBLISH_DOC_HASH, FIXTURE_NETWORK, FIXTURE_REGISTRY, FIXTURE_VERIFIERS, }; pub use manifest::{ Apply, Author, Backend, BackendError, BootstrapManifest, Deploy, KometBackend, Publish, Rotate, diff --git a/docs/recovery/README.md b/docs/recovery/README.md new file mode 100644 index 0000000..2507a86 --- /dev/null +++ b/docs/recovery/README.md @@ -0,0 +1,36 @@ +# Account recovery + +Stage 4 of the perch recovery plan: schema, shared controller, compiler +integration, and client support for opt-in account recovery (guardian, ZK, +or combined), generalizing the validated Nido Stage 3 experiment into +`perch-ir`, `perch-doc-compiler`, `crates/perch-recovery`, and +`@stellar-registry/perch`. + +**Start here if you're reviewing this stage:** + +1. [`section-7-gate.md`](section-7-gate.md) — the open, release-blocking + decision this stage does not resolve. Read this first: nothing else here + should be read as answering it. +2. [`schema.md`](schema.md) — the `recovery` document field: design, the + non-circular baseline commitment, and the canonical-form regression + guarantee for documents that don't use it. +3. [`controller-governance.md`](controller-governance.md) — the shared + `perch-recovery` controller: Variant A completion, the `guard_apply_doc` + reconfigure gate, ZK adapter scope, and what's proven end-to-end. +4. [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md) — + the constructorless/immutable requirement for the controller and any ZK + verifier, and what "upgrade" means instead of code mutation. +5. [`account-mutation-paths.md`](account-mutation-paths.md) — every entry + point that can change an account's rules or a controller's per-account + state, in one table. +6. [`migration.md`](migration.md) — why an account deployed before this + stage can never gain recovery in place, and what moving to a new one + actually requires. +7. [`formal-verification-impact.md`](formal-verification-impact.md) — Lean + and `perch-conformance` impact: what needed no change and why, and what's + explicitly scoped out with rationale rather than silently skipped. + +See also the authoritative decision record this stage implements +(`/Users/willem/c/willemneal/firstmate/data/perch-zk-recovery-scout-p5/follow-up.md` +at dispatch time) and the validated experiment it generalizes (nido +`fm/nido-recovery-stage3-n8`, PR nidohq/nido#206). diff --git a/docs/recovery/account-mutation-paths.md b/docs/recovery/account-mutation-paths.md new file mode 100644 index 0000000..6f38b96 --- /dev/null +++ b/docs/recovery/account-mutation-paths.md @@ -0,0 +1,64 @@ +# Account mutation path inventory + +Every entry point that can change a `PerchAccount`'s stored authorization +state (context rules, signers, applied `doc_hash`), or a recovery +controller's per-account state, as of this stage. This is the concrete +answer to "review... all account mutation paths." + +## The account itself (`crates/perch-account`, `crates/perch-smart-account`) + +| Entry point | File:line | What it can change | Authorization | +|---|---|---|---| +| `PerchAccount::__constructor` | `crates/perch-account/src/lib.rs:31-33` | Installs rule 0: the initial admin, `self-admin` scope. Runs once, at deploy. | Deploy-time only; not callable afterward. | +| `PerchSmartAccount::apply_doc` | `crates/perch-smart-account/src/lib.rs` (`fn apply_doc`) | **The sole write path.** Atomically replaces the entire context-rule set from a compiled document; installs or removes the `"recovery"` rule; updates `applied_doc_hash` and `recovery_controller`. | `e.current_contract_address().require_auth()` — satisfied by the ordinary admin rule, **or** by the adopted recovery controller's `enforce()` via the `"recovery"` rule (Variant A completion) — two independent, alternative authorizations for the same scope. Additionally gated by `guard_apply_doc` (below) whenever a controller is currently enrolled. | + +Every other OZ `SmartAccount` mutation entry point +(`add_context_rule`, `remove_context_rule`, `add_signer`, `remove_signer`, +`add_policy`, `remove_policy`, `update_context_rule_name`, +`update_context_rule_valid_until`) is implemented (to satisfy the +`SmartAccount` supertrait) but **not exported** — doc-only is structural, not +conventional, and proven by +`crates/integration-tests/tests/apply_doc.rs::piecemeal_mutation_entry_points_do_not_exist`. +There is no entry point reachable on a deployed `PerchAccount` that can +mutate rules other than `apply_doc`. + +`PerchStorage` (`crates/perch-smart-account/src/lib.rs`) additionally tracks +`recovery_controller: InstanceItem
` — set/cleared only inside +`apply_doc`, in lockstep with whether the applied document enrolls recovery. + +## The recovery controller (`crates/perch-recovery`) + +None of these mutate the *account's own* rule set directly — they mutate the +controller's own per-account state, which then gates or is read by +`apply_doc` (above). + +| Entry point | What it changes | Authorization | +|---|---|---| +| `Policy::install` | Writes the enrolled `CompiledRecoveryConfig` for an account. | Reachable only via `apply_doc`'s `add_context_rule` call (§ above); the actual authorization decision already happened in `guard_apply_doc` before this runs — see [`controller-governance.md`](controller-governance.md) for why `install` itself cannot be the gate. | +| `Policy::enforce` | Consumes a live, authorized, correctly-targeted attempt (marks `Completed`, revokes credentials, spends a nullifier). | Reachable only when the `"recovery"` context rule is selected for an `apply_doc` call — see `controller-governance.md`'s "Variant A completion." | +| `Policy::uninstall` | Nothing (deliberate no-op). | N/A — see `controller-governance.md` for why. | +| `guard_apply_doc` | Nothing by itself (a check); refusing it blocks the `apply_doc` call that invoked it. | Called only from `perch-smart-account`'s `apply_doc`; internally requires guardian/ZK evidence when gating a `Protected` change. | +| `begin_lost_key_attempt` / `begin_compromise_attempt` | Creates or replaces the account's attempt. | Permissionless — declaring intent carries no authority (matches the validated experiment). `begin_compromise_attempt` additionally requires a baseline to be enrolled. | +| `submit_guardian_approval` | Adds to `Attempt::guardian_approvals`; may promote to `AuthorizedPending`. | The named guardian's own `require_auth()`; must be a member of the enrolled guardian set. | +| `submit_zk_proof` | Marks `Attempt::zk_verified`; may promote. | Permissionless — a verified proof is itself the authorization; the controller recomputes the statement from its own stored attempt, never trusting a caller-supplied one. | +| `submit_guardian_cancel` | Tallies a cancel vote (separate domain from initiation, §2.2); cancels once quorum is reached. | The named guardian's own `require_auth()`. | +| `submit_zk_cancel` | Cancels on a valid proof over the `Cancel`-domain statement. | Permissionless, same rationale as `submit_zk_proof`. | +| `get_config`, `config_hash`, `get_attempt`, `has_pending` | Nothing (read-only). | None. | + +## What this means for the review + +- **Exactly one path changes what an account's admin key or app rules are:** + `apply_doc`, always. Recovery does not add a second way to set rules — it + adds a second *authorization* for the same call (the `"recovery"` rule + alongside the admin rule), and a pre-check (`guard_apply_doc`) that can + additionally require evidence before that call proceeds when a `Protected` + recovery configuration itself is what's changing. +- **No entry point anywhere lets a party without the enrolled recovery + condition author a document-level change while claiming recovery's + authority.** `enforce`'s checks (live, authorized, unexpired, exact target) + are the only way the `"recovery"` rule ever authorizes anything, and they + cannot be short-circuited by any other reachable path (see + `controller-governance.md`'s discussion of `install`/`uninstall` not being + trustworthy gates, and why the real gate sits in `apply_doc` instead). +- **The controller's own per-account state has no admin/owner mutation path + at all** — see [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md). diff --git a/docs/recovery/controller-governance.md b/docs/recovery/controller-governance.md new file mode 100644 index 0000000..a250df8 --- /dev/null +++ b/docs/recovery/controller-governance.md @@ -0,0 +1,215 @@ +# Recovery controller: design, governance, and upgrade policy + +This documents `crates/perch-recovery`: what it generalizes from the +validated Nido Stage 3 experiment, how completion actually authorizes +`apply_doc` (Variant A), the reconfigure-authorization rule that governs +every change to enrolled `Protected` recovery, and how the controller itself +is governed. + +## What's generalized from the validated experiment, and what's changed + +Nido's Stage 3 controller validated the shape this crate implements: +initiation/delay/expiry/cancel/completion state machine, guardian and ZK +evidence against one frozen proposal, `config_hash` commitment, and Variant A +completion (recovery authorizes the account's own document-apply operation +rather than a dedicated raw mutator). `crates/perch-recovery` keeps all of +that. Three things are deliberately different, each for a stated reason: + +1. **Enrollment happens through the document, not a side-channel `enroll` + call.** Nido's controller predates perch's schema support for recovery + (§3 of the follow-up review): it received `RecoveryConfig` as a direct + call argument because there was nowhere else for it to live. Now that + `perch-ir`/`perch-doc-compiler` carry it (see + [`schema.md`](schema.md)), enrollment and reconfiguration are just what + happens when `apply_doc` installs a compiled document whose `recovery` + section differs from before — the OZ `Policy::install` hook receives the + compiled config as its install params, the same mechanism every other + perch policy (the interpreter, `spending_limit`) already uses. +2. **`Protected` reconfigure is general, not additive-only.** Nido's + `reconfigure` entry point accepted exactly two transitions + (`GuardianOnly→Combined`, `ZkOnly→Combined`) and rejected everything else, + including under `Loss` — a scope decision for their bounded experiment, + not a requirement from the authoritative decision record. That record's + §2 states plainly: *"Protected configuration changes: Require ordinary + admin authorization plus the enrolled recovery condition,"* with no + restriction to additive transitions, and §2.1: *"A downgrade uses the + current, stronger requirements"* — implying a downgrade is possible, not + forbidden. `guard_apply_doc` (below) implements the general rule; the two + additive transitions Nido validated remain reachable as the common case + (they're just ordinary instances of "config changed while `Protected`," + authorized by guardian quorum with no new cryptography needed), and the + guardian-authorized path is fully general with no extra risk. The + ZK-authorized path reuses the same statement-hashing approach used for + initiation (a new domain tag, `Action::Reconfigure`, over the same kind of + 32-byte digest) — not a new cryptographic primitive, so there was no + reason to inherit Nido's narrower "no Reconfigure-domain circuit exists + yet" limitation, which was about a *specific* circuit they'd built and + tested, not an interface constraint. See "ZK adapter scope" below for what + *is* still deliberately not shipped. +3. **No circuit.** See "ZK adapter scope" below. + +## Variant A completion, precisely + +The account's `apply_doc(doc_json, recovery_evidence)` is the **only** +completion path — there is no dedicated recovery-mutation entry point. A +completed attempt is one whose `apply_doc` call was authorized through the +account's own OZ context-rule mechanism, by a **separate, zero-signer +`self-admin` rule named `"recovery"`** whose sole attached policy is the +adopted controller — installed by `perch-smart-account`'s `apply_doc` +alongside the document's ordinary rules whenever `recovery` is enrolled (see +[`account-mutation-paths.md`](account-mutation-paths.md)). + +Soroban's own context-rule selection is what makes this work: a caller's +`AuthPayload` names *which* `context_rule_id` authorizes a given call. For +the `"recovery"` rule — zero declared signers, one attached policy — OZ's +`do_check_auth` defers all validation to that policy's `enforce()` +(`stellar_accounts::smart_account::storage::get_validated_context_by_id`: +*"With policies, defer full validation to enforce()"* — confirmed by reading +the pinned dependency). So selecting the `"recovery"` rule with zero +signatures and calling `apply_doc(target_doc_json, ...)` reaches +`PerchRecovery::enforce`, which: + +1. Reads the account's current attempt; refuses unless it is + `AuthorizedPending`. +2. Refuses if the ledger sequence is before `executable_after` (timelock) or + at/after `expires_at`. +3. Refuses unless `sha256(target_doc_json)` equals the attempt's frozen + `target_doc_hash` — set once, at `begin_lost_key_attempt`/ + `begin_compromise_attempt`, never recomputed. +4. Only then: marks the attempt `Completed`, appends its + `replaced_credentials` to the account's permanent revoked set, spends its + nullifier (if ZK-sourced) — atomically, as part of authorization + succeeding, before `apply_doc`'s body (the compile/install) even runs. + +**Why the ordinary admin rule can't shortcut this:** the admin rule and the +recovery rule are independent, alternative authorizations for the same +`CallContract(self)` scope. An ordinary admin-authorized `apply_doc` call +never touches `enforce()` at all (it selects the *admin* rule); it can apply +any document the admin chooses to sign for, exactly as before recovery +existed — that is normal account administration, not a recovery bypass. +What `enforce()` prevents is a call *claiming* the recovery rule's authority +(zero signers, relying on the policy) without a live, correctly-targeted, +timelock-elapsed attempt behind it — proven directly in +`crates/integration-tests/tests/recovery.rs` by driving `do_check_auth` +with an explicit rule selection (mirroring `matrix.rs`'s own pattern), the +same mechanism the host uses for a real `__check_auth` invocation. + +**Why atomicity holds without a completion-grant flag:** everything in step +4 happens inside the same top-level Soroban invocation as `apply_doc`'s own +body. If the document fails to compile or install (any `DocCompilerError`, +the anti-brick check), the whole transaction reverts — including the +attempt-consuming mutation above — so there is no reachable state where the +attempt is spent but the account's rules are unchanged. This closes the +follow-up review §3.1 gap by construction (no ledger-scoped grant to leave +stale or ungated) rather than by hardening a fragile flag. + +## The `guard_apply_doc` gate and why it — not `install`/`uninstall` — is authoritative + +`stellar_accounts::smart_account::storage::remove_context_rule` calls a +policy's `uninstall` via `try_uninstall` and **discards the result even if +it panics** (confirmed by reading the pinned dependency directly — this is +exactly the class of gap the follow-up review §3.2 flags in a different +codebase). Since `apply_doc` wipes and reinstalls the *entire* rule set on +every call, a policy that tried to block a `Protected` account's +reconfiguration from inside `uninstall` would simply be ignored, and the +rule would be removed anyway. + +The actual gate is `perch-smart-account`'s `apply_doc`, which — whenever +`PerchStorage::recovery_controller` names a currently-adopted instance — +cross-calls that instance's `guard_apply_doc(account, new_recovery, +recovery_evidence)` **before** touching any context rule: + +1. **Unconditional:** if a live attempt exists (`CollectingEvidence`, or + `AuthorizedPending` and not yet expired), refuse. This is property 9 + (configuration consistency), independent of §7's still-open freeze/continue + question — see [`section-7-gate.md`](section-7-gate.md) — and it is what + makes `apply_doc`'s own timing "for free" for completion: `enforce` (§ + above) already flips a legitimate completion's attempt to `Completed` + *before* `guard_apply_doc` runs, so this check reads `false` for exactly + that call and blocks every other concurrent or racing call. +2. **No change, or no prior enrollment:** always fine — establishing new + protection, or reapplying an unchanged configuration (including as a side + effect of a legitimate completion, whose target document carries the same + `recovery` section as before), needs nothing extra. +3. **Currently `Loss`:** ordinary admin authorization (already established + by `apply_doc`'s own `require_auth`) is sufficient for *any* new + configuration, including removing it — §2.1. +4. **Currently `Protected`:** requires the *currently* enrolled condition's + evidence over a digest binding `(account, controller, Reconfigure, + old_config_hash, new_config_hash_or_removal)` — guardian quorum via + `require_auth_for_args` for a guardian-capable mode, a ZK proof over the + same digest for a ZK-capable mode, both for `Combined`. See + `crates/integration-tests/tests/recovery.rs`'s + `protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused` + for this proven end-to-end (admin alone, no evidence, is refused). + +`install`/`uninstall` are therefore plain bookkeeping: `install` shape-checks +the rule it's attached to and writes the config (the gate already ran); +`uninstall` is a deliberate no-op. + +## ZK adapter scope + +`crates/perch-recovery/src/zk.rs` fixes the **statement** a proof must be +over (network, account, controller, action, config hash, target-or-removal, +attempt nonce, delay — every field the follow-up review §5.3 names for a +proposal commitment) and a minimal, proof-system-agnostic verifier interface +(`verify_proof(statement, nullifier, proof, pool) -> bool`). It ships **no +circuit** and is not tested against one. This is deliberate, per §5.4: +*"Proof-system portability is an interface goal; supporting multiple proof +systems in the first release is not a requirement."* Building and +validating an actual circuit is real cryptographic work with its own review +needs, out of proportion to a schema-and-controller stage; the interface is +designed so a future circuit can be dropped in (any verifier satisfying this +trait) without changing the controller. `ZkOnly`/`Combined` modes are fully +represented in storage, compiled correctly, and gated identically to +`GuardianOnly` — only the "does a real ZK circuit exist to generate proofs +against" question is out of scope here, tracked as follow-up work. + +## Controller governance: no admin, ever + +The controller has no constructor, no owner/admin storage key, and no entry +point that changes its own code's behavior globally — see +[`vk-and-controller-immutability.md`](vk-and-controller-immutability.md) for +the full requirement and why. Per-account state changes only through: the +account's own authorization (`install`, gated by `guard_apply_doc` upstream), +the enrolled guardians' own authorization (`submit_guardian_approval`, +`submit_guardian_cancel`), or a valid proof against the enrolled verifier +(`submit_zk_proof`, `submit_zk_cancel`). "Upgrading" the controller is +deploying a new instance and an account explicitly adopting it through its +own document — never anything at the existing address. + +## Adversarial properties proven end-to-end + +`crates/integration-tests/tests/recovery.rs` (guardian-only mode, driving +the real `do_check_auth`/`Policy::enforce` path): + +- No attempt at all → completion refused. +- Below guardian quorum → completion refused, and does not promote. +- Quorum reached, before the timelock elapses → completion refused. +- Quorum reached, wrong target document → completion refused, attempt not + consumed. +- Quorum reached, at/after the timelock, exact target → completes. +- Replaying the same completion → refused (already `Completed`). +- An attempt pending blocks an ordinary `apply_doc` call unconditionally. +- Guardian quorum exactly at the boundary (2 of 3, then 3 of 3) promotes + only once quorum is actually reached. +- Cancellation evidence is a separate domain from initiation evidence — an + initiation approval does not count toward cancelling the same attempt. +- A cancelled attempt allows a fresh one afterward. +- A `Protected` reconfiguration (here: disabling recovery entirely) with + ordinary admin authorization alone is refused. + +`crates/perch-ir/tests/recovery.rs` and `crates/perch-recovery` (unit-level, +via `perch_doc_compiler` types) cover the schema/wire side: guardian-only +carries no ZK field at all, `zk-only` carries no guardian field, quorum +bounds, credential-fingerprint resolution, and the canonical-form regression +for documents without recovery. + +Not yet covered (tracked, not silently assumed sound): a live ZK circuit +exercising `submit_zk_proof`/`submit_zk_cancel` against a real verifier (no +circuit is shipped — see "ZK adapter scope"); a rule-teardown scenario where +`uninstall` itself panics (this crate's `uninstall` is a no-op by design, so +the property to check is narrower than Nido's — that removal always +succeeds regardless — which follows directly from `uninstall` never being +able to fail); and the full §7 pending-activity enforcement beyond policy +mutation (see [`section-7-gate.md`](section-7-gate.md)). diff --git a/docs/recovery/formal-verification-impact.md b/docs/recovery/formal-verification-impact.md new file mode 100644 index 0000000..93aab81 --- /dev/null +++ b/docs/recovery/formal-verification-impact.md @@ -0,0 +1,90 @@ +# Formal-verification and conformance impact of the recovery schema + +Per repo verification conventions (`formal/README.md`, `docs/verification/PLAN.md`, +`docs/verification/THEORY.md`), every schema or semantics change is expected +to state its impact on the Lean model and `perch-conformance` explicitly — +covered, or scoped out with rationale. This is that statement for the +`recovery` field added to `PolicyDoc`. + +## perch-conformance / eval-vectors: no impact, by design + +`perch-conformance`'s vectors (`testdata/eval/eval-vectors.json`) and the +Lean `Semantics.lean`/`Lowering.lean`/`Theorems.lean` (T1–T6) model +**perch-program evaluation** — the RPN machine `__check_auth` runs per +invocation. `RecoveryConfig` never lowers to a perch-program op: it is not +installed as, or referenced by, any interpreter program. This isn't an +oversight; it's the load-bearing design decision from the follow-up review +§1 and §5.1 ("Recovery state remains outside the stateless interpreter"), +and it is *why* recovery is architecturally possible at all — +`docs/verification/THEORY.md`'s enforceability argument states plainly that +no execution monitor, including perch's, can enforce "the account can +always recover" (a liveness property), and that perch "addresses the +adjacent risk structurally instead" by keeping INV-2's policy-free admin +path independent of the interpreter. A recovery controller *is* that +structural answer, implemented as ordinary stateful contract logic outside +perch's own enforceable-fragment boundary — it was never a candidate for +inclusion in the per-invocation-safety-property model in the first place. + +Concretely: `crates/perch-compile` (the crate that lowers `PolicyDoc` rules +into perch-program `InstallParams`) is untouched by this stage — recovery +lowering happens entirely inside `perch-doc-compiler`, mapping +`perch_ir::RecoveryConfig` directly to a new wire type +(`CompiledRecoveryConfig`) with no perch-program involvement. So T1–T6 and +the eval-vectors conformance suite need no new cases and remain sound +exactly as before. + +## Lean `Canon.lean` / `CanonProofs.lean` (T7): explicitly scoped out, with rationale + +`Canon.lean` is a hand-maintained Lean *twin* of `perch-ir`'s canonical-form +emitter (`crates/perch-ir/src/canon.rs`), and `CanonProofs.lean` proves +`emitDoc_injective` (T7) against the Lean model's own `Doc` type — not +against the Rust type directly. Extending Lean's `Doc` to add a `recovery` +field (plus the `RecoveryMode`/`GuardianSet`/`ZkVerifierConfig`/ +`BaselineCommitment`/`PendingActivityPolicy` sub-types) and re-proving +injectivity over the enlarged domain is real, non-mechanical proof +engineering: a new sum-of-products shape enters the emitter and its +verified inverse parser, and `emitDoc_injective`'s proof structure would +need new cases throughout. + +**This stage does not do that work**, for two reasons stated plainly rather +than hidden: + +1. **Correctness bar.** `formal/README.md` states every theorem here is + "sorry-free." Extending a non-trivial injectivity proof under time + pressure risks landing an admitted or subtly-wrong proof, which is worse + than an honest gap — a `sorry` (or a proof that typechecks but doesn't + actually establish what it claims) would silently narrow what T7 is + worth without anyone noticing on a green CI run. +2. **No loss to the existing claim.** T7 continues to hold, exactly as + proved, for every document Lean's `Doc` type can represent — which is + every document *without* recovery. The Rust-side regression test + (`crates/perch-ir/tests/recovery.rs::recovery_absent_documents_hash_exactly_as_before_this_field_existed`, + backed by the unchanged `ci-publish{,-delegated,-threshold}` golden + vectors) is exactly the condition that keeps this true: nothing about + adding an *optional*, omitted-when-`None` field changes the canonical + bytes of any document that doesn't use it. T7's proof over that domain + is not weakened, invalidated, or cast into doubt by this change — it + simply does not yet extend to the new domain. + +**What this means concretely:** `just drt` (the differential Rust↔Lean +replay) is unaffected and continues to pass, because it replays +`testdata/eval/eval-vectors.json` and round-trips the existing +(recovery-absent) `ci-publish*.canonical.json` fixtures — none of which +gained a `recovery` field. The two new fixtures added by this stage +(`ci-publish-recovery.json`, `ci-publish-recovery-combined.json`) are +**not** round-tripped through the Lean model, because Lean's `Doc` cannot +represent them yet. Their canonical-form and hash agreement is instead +verified the same way every other cross-language fixture in this repo is — +by the Rust (`crates/perch-ir/tests/recovery.rs`) and TypeScript +(`packages/perch-js/test/parity.test.ts`) suites agreeing on committed +bytes — which is real cross-implementation conformance, just not +machine-checked against the Lean model. + +## Tracked follow-up (not this stage) + +Extending `Canon.lean`/`CanonProofs.lean` to cover `RecoveryConfig` and +re-establishing `emitDoc_injective` over the enlarged domain is legitimate +future work, tracked here rather than attempted under this stage's time +budget. It should be undertaken as its own reviewed change with room to get +the injectivity argument right, not bundled into a schema-and-controller +release. diff --git a/docs/recovery/migration.md b/docs/recovery/migration.md new file mode 100644 index 0000000..6facf10 --- /dev/null +++ b/docs/recovery/migration.md @@ -0,0 +1,135 @@ +# Migrating existing accounts to recovery support + +**Audience:** wallets, integrators, and account owners deciding whether an +already-deployed `perch-account` can gain recovery support in place, or needs +a new account. + +**Hard rule this document follows throughout:** shipping this release does +not, and cannot, change the behavior of any already-deployed `PerchAccount` +or already-deployed `perch-doc-compiler` instance. Both are constructorless +and immutable by construction (see +[`vk-and-controller-immutability.md`](vk-and-controller-immutability.md)). +Nothing below is a claim that a future library release changes deployed +accounts — it is the opposite claim, worked out to its consequences. + +## Why a deployed account cannot pick up new schema support in place + +`PerchAccount::apply_doc` (`crates/perch-smart-account/src/lib.rs`) resolves +the document compiler it calls **offline, at build time**, not from any +runtime-configurable address: + +```rust +let interpreter = infra::perch_interpreter::address(e); +let compiled: CompiledDoc = + DocCompilerClient::new(e, &infra::perch_doc_compiler::address(e)) + .try_compile_doc(&doc_json)??; +``` + +`infra::perch_doc_compiler::address(e)` (via +`perch_registry_resolve::registry_contract!`, `crates/perch-smart-account/src/lib.rs:59-61`) +derives `deployer(stateless_registry_id, sha256(pinned_wasm_bytes))` where +`pinned_wasm_bytes` is read from a file fetched into the build tree by +`scripts/fetch-infra-wasm.sh` **before `perch-account` is compiled**. That +hash — and therefore the exact compiler address — is baked into the +account's own WASM. It is not stored in instance storage, not passed as a +constructor argument, and has no setter. `perch-account`'s own module doc is +explicit about the consequence: *"The account is not upgradeable and has no +execution entry point: replacing it means deploying a new account and moving +[external] ownership."* + +Two independent immutable artifacts must both understand recovery for +enrollment to be possible at all: + +1. **The doc-compiler instance the account calls.** A pre-Stage-4 compiler's + own statically-linked `perch-ir` has no `recovery` field in its + `deny_unknown_fields` list (`crates/perch-ir/src/parse.rs`) — a document + containing a `"recovery"` key is rejected as an unknown field, on-chain, + before anything else happens. This is not a permissions error; the old + compiler genuinely does not know the shape exists. +2. **The account's own `apply_doc` logic.** Even if a document could get + past compilation, a pre-Stage-4 account's `apply_doc` has no code path + that calls a recovery controller — that call only exists in a + post-Stage-4 build of `perch-smart-account`/`perch-account`. + +Consequently: **a `PerchAccount` deployed before this release can never +enroll recovery through its own `apply_doc`, under any circumstances.** +This is a permanent property of that deployment, not a temporary gap this +document works around. + +## The two cases + +### Case A — account built before this release + +Recovery is permanently unavailable in place. The only path is: + +1. Deploy a new `PerchAccount` from a post-Stage-4 build. Its constructor + takes the same shape as before (`admin_signers: Vec`) — the + user's existing signer keys/credentials are reused as-is; nothing about a + WebAuthn credential, ed25519 key, or delegated address is + perch-version-specific. +2. Submit the desired document (the old account's document content, plus a + `recovery` section) to the **new** account via its `apply_doc`. Because + `RuleScope::SelfAdmin` resolves to `e.current_contract_address()` at + apply time rather than being baked into the compiled rule + (`crates/perch-doc-compiler/src/lib.rs`'s `RuleScope` has no + account-address field at all), and `Scope::Contract` rules name some + *other* contract's address, not the account's own — **the exact same + document JSON the old account was running compiles and installs + identically on the new account.** A policy document is portable across + this kind of migration by construction; only the `recovery` section (and + any deliberate policy change) needs to be added. +3. Move everything that isn't inside the document: + - **Balances.** A contract account's balance lives at that contract's + address. Moving it is an explicit transfer transaction, signed under + the *old* account's still-valid authorization, sent to the *new* + account's address. Do this before decommissioning the old account — + once its own admin rule can no longer be satisfied (e.g. after signer + rotation elsewhere), it cannot authorize anything, including its own + exit transfer. + - **External references to the old account's address** — anything + outside perch that stores "the owner/session/allowlisted caller is + ``" (other contracts' stored state, a dApp's session + table, a name/registry entry an integrator maintains for their own + users). Perch has no visibility into these; enumerating and + re-pointing them is integration-specific and is the responsible + wallet/integrator's job, not something perch's contracts can automate. +4. Treat the old account as retired. It cannot be deleted (no + self-destruct, no upgrade path); it simply stops being referenced. + +**Practical note for tooling:** because there is no on-chain field recording +which `perch-account`/`perch-doc-compiler` build an account was deployed +against, a wallet cannot currently *discover* whether a given account is +Case A or Case B by inspecting the account alone. Until perch grows a +queryable version marker (tracked as follow-up work, not part of this +release), integrators should record the release each account was deployed +from at deploy time (e.g. alongside however they already track deployments — +see `DEPLOYED.md`-style records used elsewhere in this repo's tooling) and +consult that record rather than guessing. + +### Case B — account built from this release or later, recovery not yet enrolled + +No migration is needed. Recovery is enrolled, changed, or removed the same +way any other policy change is made: submit a new document via `apply_doc`. +The [profile-based authorization rules](controller-governance.md) govern +*who* may make that change (ordinary admin alone under `loss`; ordinary +admin plus the currently-enrolled recovery condition under `protected`) — +there is never a "new account" requirement for this case. + +## What does *not* need to migrate + +Nothing recovery-specific, because a Case A account never had any: no +enrolled `RecoveryConfig`, no controller state, no baseline, no guardian +set, no pending attempt. There is no in-place recovery state to carry over +— only the account's existing (non-recovery) document content, which is, as +shown above, portable as-is. + +## What this document does not claim + +- It does not claim perch can make a pre-Stage-4 account upgradeable after + the fact. It cannot, by design (see + [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md)). +- It does not claim balance or external-reference migration is automated by + perch tooling. Both are explicit, integration-specific steps an + integrator must perform. +- It does not claim every Case A account *should* migrate. An account owner + who doesn't need recovery has no reason to. diff --git a/docs/recovery/schema.md b/docs/recovery/schema.md new file mode 100644 index 0000000..918e7a6 --- /dev/null +++ b/docs/recovery/schema.md @@ -0,0 +1,173 @@ +# Recovery configuration: schema design + +This documents the `recovery` field added to `PolicyDoc` +(`crates/perch-ir/src/doc.rs`), why it is shaped the way it is, and how it +was fit into `CANONICAL.md` without disturbing the canonical form of any +document that doesn't use it. See the authoritative decision record's §5.5 +("Reviewable configuration without circular hashes") for the requirements +this design answers. + +## What's in scope here, and what isn't + +`RecoveryConfig` is **enrollment configuration** — who may recover this +account and how. It is not a recovery attempt. An attempt's target document +and evidence are supplied when recovery starts, against a separate +controller contract (see [`controller-governance.md`](controller-governance.md)); +none of that is, or should be, predeclared in the document. Putting only the +configuration in the document — never dynamic pending state — is what keeps +`doc_hash` a stable, reviewable identity: two enrollments with the same +configuration hash the same, regardless of how many attempts have come and +gone. + +## Field-by-field rationale + +``` +recovery: Option +``` + +Omitted entirely when `None` (see "CANONICAL.md treatment" below) — this is +the single fact that keeps every pre-existing document's hash unchanged. + +``` +RecoveryConfig { + profile: RecoveryProfile, // Loss | Protected — §2.1 + mode: RecoveryMode, // GuardianOnly | ZkOnly | Combined + controller: String, // adopted controller instance's address + baseline: Option,// required to enroll compromise recovery + replaceable: Vec, // signer ids recovery may replace — §4.1 + delay_ledgers: u32, // timelock window + expiry_ledgers: u32, // authorized-attempt lapse window + max_cancels: u32, // lifetime cancellation cap (griefing bound) + pending_activity: PendingActivityPolicy, // Freeze | Continue, NO default — §7 +} +``` + +- **`profile` and `mode` are orthogonal fields**, exactly per §2.1 ("Either + profile can use any of the three authentication modes"). Keeping them as + two independent fields rather than a combined enum avoids a 6-way + cross-product type for no benefit. +- **`mode` is a per-variant enum, not a flattened struct with optional ZK + fields.** The validated experiment this stage generalizes stored mode as + one struct with `guardians: Vec
` and `verifier: Option
` + side by side (`RecoveryConfig` in that codebase's `types.rs`), because its + target SDK's `#[contracttype]` derive doesn't support `Option` + on a flattened struct field cleanly. Perch's document model has no such + constraint (`perch-ir` is plain Rust, not itself a `#[contracttype]`), so + `RecoveryMode::GuardianOnly(GuardianSet)` simply **cannot** carry a + `verifier`/`circuit-id`/`pool` field at all — "guardian-only requires no ZK + machinery" is enforced by the type, not by a runtime check that a ZK field + happens to be `None`. The wire (`perch-doc-compiler`) and JSON + (`perch-js`) encodings flatten the tagged variant's fields into one object + at the wire level (`{"type":"guardian-only","guardians":[...],"quorum":N}`) + for a compact document, but the *source of truth* — the Rust and TS types + — stay per-variant. +- **`delay_ledgers`/`expiry_ledgers` are ledger-sequence deltas, not + durations in seconds.** This matches perch's own existing convention + (`Rule::not_after_ledger`) rather than the validated experiment's + timestamp-based fields — the account, interpreter, and every other + ledger-relative quantity in this schema already speaks ledger sequence, + and mixing units within one document would be a real (if subtle) foot-gun + for anyone reading it. +- **`replaceable` is required non-empty**, per §4.1: "Enrollment records... + identifies which signer entries or roles recovery may replace." An empty + list would enroll recovery that can never restore access — rejected at + validation, not left as a silent no-op. In the document it names + document-local signer **ids** (human-reviewable, e.g. `"admin"`); the + compiler resolves each to a `sha256` fingerprint of that signer's actual + credential (verifier+key, or the delegated address) for the wire form the + controller stores — not the id string, which a later document could + legitimately reuse for a different physical key. This is what lets + revocation (property 10: an old baseline must not restore a revoked + credential) survive id reuse across documents. +- **`baseline` is optional, and its presence is what gates suspected-compromise + recovery**, per §4.2: lost-key recovery needs no predetermined baseline + (it targets "current approved document with designated credentials + replaced"); compromise recovery needs one to restore to. `None` therefore + means "lost-key recovery only," not "recovery is half-configured." +- **`pending_activity` has no default and no third "unspecified" variant.** + See [`section-7-gate.md`](section-7-gate.md) — this is the schema-level + half of keeping §7 an explicit parameter rather than a library default. + +## Non-circular baseline commitment (§5.5) + +`BaselineCommitment { doc_hash: String }` names a **different, earlier** +document's canonical hash — never the enclosing document's own hash. There +is no field anywhere that could make a document's `recovery.baseline` +recursively depend on that same document's own `doc_hash`, because the +baseline is always a value chosen *before* the enclosing document is +authored (whatever the previously-approved document was), and `doc_hash` is +computed *after* the whole document — including `recovery` — is fixed. The +`ci-publish-recovery` conformance fixture demonstrates this concretely: its +`recovery.baseline.doc-hash` is literally the pinned `doc_hash` of the +plain `ci-publish` fixture, a genuinely different, independently-hashed +document (see `testdata/ci-publish-recovery.json` and +`crates/perch-ir/tests/recovery.rs`). + +This is intentionally **not** verified against on-chain history at compile +time — `perch-smart-account` only stores the *current* `applied_doc_hash`, +not a history of prior ones (see +[`account-mutation-paths.md`](account-mutation-paths.md)), so there is +nothing on-chain to check a baseline against even if perch wanted to. +Reviewing that a declared baseline hash genuinely names a real, +previously-approved document is part of enrollment review, the same way +reviewing that a `Scope::Contract` address is the intended contract is part +of ordinary rule review. + +## CANONICAL.md treatment + +`recovery` slots into the existing "omit `None`, don't emit `null`" +convention (`CANONICAL.md`, "Booleans and null") that every other optional +field in the schema already uses (`network`, `not-after-ledger`, `cap`, +...). No new canonicalization *rule* was needed — `crates/perch-ir/src/canon.rs` +adds a `recovery_to_cv` builder that's only invoked `if let Some(r) = +&doc.recovery`, exactly mirroring `rule_to_cv`'s treatment of `cap`. Because +`CANON_VERSION` is a format identifier covering the *serialization rules*, +not the document's field set, adding a new optional field does not bump it +— the rules in `CANONICAL.md` (object key sorting, JCS string escaping, +plain-decimal `u32`s, no `null`) are unchanged; only the set of fields a +`PolicyDoc` can carry grew, which every implementation must still agree on +byte-for-byte (verified below). + +**Testable compatibility condition, and its proof:** the follow-up review's +§5.5 asks whether an additive field can preserve the existing canonical +format — a testable condition, not a blanket claim. It's tested directly: +`crates/perch-ir/tests/recovery.rs::recovery_absent_documents_hash_exactly_as_before_this_field_existed` +asserts the canonical form of a recovery-absent document contains no +`"recovery"` substring at all, and every pre-existing fixture +(`ci-publish{,-delegated,-threshold}`) and its pinned hash is **unchanged** +by this stage (not regenerated) — their tests in `crates/perch-ir/tests/fixture.rs` +and `packages/perch-js/test/parity.test.ts` still pass against the same +committed bytes. That is the condition being claimed, made byte-for-byte +verifiable rather than asserted. + +## Cross-language conformance + +Two new fixture triples extend `testdata/` in lockstep with the existing +`ci-publish*` ones: `ci-publish-recovery` (guardian-only, `protected` +profile, with a baseline) and `ci-publish-recovery-combined` (combined +guardian+ZK, `loss` profile, no baseline — demonstrating lost-key-only +enrollment). Both are asserted byte-identical and hash-identical on the +Rust side (`crates/perch-ir/tests/recovery.rs`) and the TypeScript side +(`packages/perch-js/test/parity.test.ts`), and `packages/perch-js/src/builder.ts`'s +new `.recovery(...)` builder method is proven to reproduce both fixtures' +exact `doc_hash` from a fluent call +(`packages/perch-js/test/builder.test.ts`) — the same "does the ergonomic +builder actually produce the reviewed bytes" property already established +for every other rule shape. + +perch-ir's semantic validation (`crates/perch-ir/src/validate.rs`) is +extended with recovery-specific checks (non-empty/non-duplicate +`replaceable` referencing declared signers, non-zero delay/expiry/max-cancels, +address-shape checks on `controller`/guardian addresses/verifier/pool, +hex-format checks on `circuit-id` and the baseline `doc-hash`, and +`1 <= quorum <= guardians.len()` mirroring `Principals::Threshold`'s +existing `InvalidThreshold` rationale). `packages/perch-js/src/schema.ts` +mirrors the *shape* of these rules (strict per-variant objects, required +`pending-activity` with no default) but not yet the full semantic pass — +consistent with, and tracked under, the same pre-existing gap noted at the +top of `schema.ts` for the rest of the schema (issue #8: perch-js does not +yet mirror `perch-ir`'s full `validate()`). + +## Lean / formal-verification impact + +See [`formal-verification-impact.md`](formal-verification-impact.md). diff --git a/docs/recovery/section-7-gate.md b/docs/recovery/section-7-gate.md new file mode 100644 index 0000000..1c494f5 --- /dev/null +++ b/docs/recovery/section-7-gate.md @@ -0,0 +1,81 @@ +# Release gate: pending-activity policy (open, release-blocking) + +**Status: OPEN. This is a release-blocking gate, not a design note.** No +recovery-enabled account should be deployed to production, and no +integration should advertise recovery as safe to rely on, until this gate is +explicitly resolved by the party the follow-up review named to resolve it +(the captain) — not by this codebase, not by a library default, and not by +this PR. + +This mirrors the authoritative decision record's §7 verbatim: *"Decision +status: OPEN, deliberately deferred by the user. No default selected."* That +review also warns explicitly: *"Do not infer an answer from expiry +behavior, the protection profile name, or the fact that this write-up is +finished."* The same warning applies to this codebase shipping: **do not +infer that landing this PR resolves the question.** + +## What is open + +Whether ordinary account-authorized execution, and ordinary policy-mutation +attempts, continue or are blocked while a recovery attempt is pending — and +whether lost-key and suspected-compromise attempts should behave +differently on this axis. Three candidate behaviors were on the table: +freeze, continue, and restrict-selected-operations. None was selected. + +## What this codebase does and does not do about it + +- **The schema forces an explicit, no-default choice per account.** + `RecoveryConfig::pending_activity` (`crates/perch-ir/src/doc.rs`) is a + required field with exactly two variants, `Freeze` and `Continue`, and no + `Default` implementation. `restrict-selected-operations` is deliberately + **not modeled** — adding it needs an explicit capability-boundary design + this stage did not do, so it is out of scope rather than half-built. An + enrolling document must name one of the two modeled choices; there is no + way to enroll recovery without making this choice reviewable in the + document itself. +- **Blocking ordinary *policy-document* mutation while an attempt is + pending is not part of this open question, and is already enforced + unconditionally.** The controller refuses `apply_doc` calls not + authorized by the recovery evidence itself while an attempt is pending — + see [`controller-governance.md`](controller-governance.md) — regardless + of the enrolled `pending_activity` value. This is required for property 9 + (configuration consistency) independent of §7: without it, an admin could + race a pending attempt with a conflicting document change, which §7 never + proposed making optional. `pending_activity` governs a narrower, + genuinely-open question: **ordinary contract calls the account authorizes + that are not policy mutation at all** (moving funds, calling another + contract) — the freeze/continue axis §7 actually names. +- **`Freeze`/`Continue` are recorded and queryable, not fully wired end to + end.** The controller exposes the pending attempt's state and the + enrolled policy so an integration can check it. Actually *enforcing* + `Freeze` against arbitrary non-recovery context rules would require + threading a pending-recovery check through perch-interpreter's + context-rule evaluation for every rule, not only the recovery-authorizing + one — a cross-cutting change to the interpreter's evaluation path that is + explicitly **not implemented in this stage**. Until it is, an account + enrolled with `Freeze` records that intent reviewably, but does not yet + have it mechanically enforced against every other rule on the account. + Shipping that enforcement is itself gated on §7 actually being resolved + (there is no point hardening a mechanism for a policy that might change). + +## What must happen before production reliance + +1. The captain (or whoever the follow-up review names as the decision + owner) resolves §7: freeze, continue, restrict, or some other + precisely-defined rule — including the related, separately-named + question of what happens to a *stale lost-key source snapshot* when + ordinary policy writes are attempted during a pending attempt (block, + invalidate the attempt, or another explicit conflict rule; see the + follow-up review §4.2 and §7). +2. If the resolution requires enforcement beyond what `pending_activity` + already records (i.e. anything beyond "freeze policy mutation," which is + already unconditional), that enforcement is designed and implemented as + its own reviewed change — not inferred from this document or shipped + silently alongside an unrelated change. +3. Only after both of the above should `Protected`-profile, `Freeze`- or + `Continue`-enrolled accounts intended to resist a compromised admin be + treated as production-ready for that specific guarantee. + +Until then: this schema field and the controller's current behavior are +available for experimentation, review, and further design work — not for a +production deployment relying on the pending-activity guarantee to hold. diff --git a/docs/recovery/vk-and-controller-immutability.md b/docs/recovery/vk-and-controller-immutability.md new file mode 100644 index 0000000..726d6ea --- /dev/null +++ b/docs/recovery/vk-and-controller-immutability.md @@ -0,0 +1,116 @@ +# VK, controller, and code immutability review + +Per the authoritative decision record §5.4 ("Constructorless verifier and +explicit upgrades") and the captain's instruction to review "code and VK +immutability, controller governance," this document states the invariant +this stage requires, shows the precedent already established elsewhere in +this repo, and states exactly how the new `perch-recovery` crate and any ZK +verifier meet it. + +## The invariant + +> A verifier (or controller) in Perch's constructorless registry remains +> immutable until an explicit upgrade. An upgrade is deploying a new +> immutable artifact and explicitly adopting its identity through the +> account's own document — never rewriting code, admin state, or verification +> configuration at an already-adopted address. + +Three consequences follow directly, and are treated as hard requirements +for anything this stage ships: + +1. **No constructor that sets mutable configuration.** If a contract's + verification behavior (a VK, a circuit identity, a policy the controller + enforces) can be set once at construction and never again, that's fine — + but if it can be set *and later changed* by anyone, including the + deployer, the artifact is not immutable, no matter how it was deployed. +2. **No admin/owner entry point at all.** An "admin-gated upgrade" is not + the same guarantee as immutability — it's a promise that the admin key + won't be misused, which is exactly the kind of trust the "no mutable + shared controller... may silently bypass commitment" language in §5.4 + rules out. The bar is that there is **nothing to misuse**: no pause + switch, no fee/parameter setter, no logic branch keyed by any mutable + flag. +3. **Identity is content, not address history.** Deploying at a + content-addressed address (`deployer(stateless_registry_id, + sha256(wasm))`, as `perch-doc-compiler` and `perch-interpreter` already + do — `crates/perch-smart-account/src/lib.rs`'s `infra` module) means the + address itself is a function of the code. Two different builds are two + different addresses; there is no way to have "the same address, new + code" at all, which is a stronger guarantee than an admin merely + *promising* not to redeploy at a pinned address. + +## Precedent already in this repo + +`perch-doc-compiler` and `perch-interpreter` already meet this bar today, +and are the model this stage follows rather than invents: + +```rust +#[cfg(feature = "contract")] +#[contract] +pub struct PerchDocCompiler; + +#[cfg(feature = "contract")] +#[contractimpl] +impl PerchDocCompiler { + pub fn compile_doc(e: &Env, doc_json: Bytes) -> Result { + // pure: no auth, no storage, no admin + } +} +``` + +(`crates/perch-doc-compiler/src/lib.rs`) — no `__constructor`, no storage +write, no auth check. Its own module doc states the property directly: +*"deployed once per network, immutable, and shared by every perch +account."* `perch-account`'s own accessor for it +(`infra::perch_doc_compiler::address`) derives the address fully offline +from a build-time-pinned WASM hash — see +[`migration.md`](migration.md) for why that specific mechanism is also what +makes a schema change require a new deployed instance rather than an +in-place change. + +## What this stage adds, and how each piece meets the bar + +- **The recovery controller (`perch-recovery`).** No constructor beyond + whatever the Soroban toolchain requires for deployment plumbing; no + admin/owner storage key anywhere in its schema; no entry point that + changes its own code's behavior globally. Per-account state + (`Config(Address)`, `Attempt(Address)`, ...) is mutated only through the + account's own authorization (ordinary admin, or the enrolled recovery + condition, depending on the operation) — never through anything resembling + a controller-wide admin key. See + [`controller-governance.md`](controller-governance.md) for the exact + entry points and their authorization requirements. +- **A ZK verifier, if a deployment enrolls a ZK-involving mode.** The bar + from §5.4 is explicit: *"Embedding the VK and proof-format identity in the + artifact is a straightforward candidate... Merely removing a constructor + while retaining mutable verification configuration is insufficient."* + Concretely, any verifier a `ZkVerifierConfig.verifier` names must: + - embed its verification key as a compiled-in constant (e.g. + `include_bytes!` at build time), never a storage value a constructor or + any other entry point writes; + - expose no entry point that changes which VK or circuit it accepts; + - commit to its circuit/proof-format identity in a form the account's own + document also carries (`ZkVerifierConfig.circuit_id`, §5.4's "defense in + depth" binding) — so a verifier address confused with another still + fails the circuit-identity check, not just the address check. + + This stage does not ship a production circuit or its verifier (see + [`controller-governance.md`](controller-governance.md)'s "ZK adapter + scope" section for why that's a deliberate, documented boundary, not an + oversight) — but the controller's ZK adapter interface is written against + exactly this contract, and is exercised in tests against a stand-in + verifier that satisfies it. + +## "Upgrade" means adoption, not mutation + +Concretely, for both the controller and any verifier: a new version is a +**new build → a new content-addressed instance → a new address**. Nothing +about deploying that new instance changes any already-enrolled account's +behavior. An account adopts it — meaning: it appears in that account's *own* +document (`recovery.controller`, or nested inside `recovery.mode`'s +`verifier` field), which the profile-gated authorization rules in +[`controller-governance.md`](controller-governance.md) apply to exactly like +any other recovery-configuration change. There is no "everyone +auto-upgrades" path, by construction — an account that never re-applies a +document naming the new instance keeps using the old one, forever, exactly +as immutable-artifact semantics require. diff --git a/packages/perch-js/src/builder.ts b/packages/perch-js/src/builder.ts index dfb72e0..250ab64 100644 --- a/packages/perch-js/src/builder.ts +++ b/packages/perch-js/src/builder.ts @@ -134,10 +134,75 @@ export class RuleBuilder { } } +/** How a recovery attempt is authorized, camelCase like the other builder + * inputs; {@link PolicyBuilder.recovery} maps it onto the kebab-case, + * type-tagged wire shape. Guardian-only carries no ZK field to leave unset, + * and zk-only carries no guardian field — mirroring perch-ir's + * `RecoveryMode` enum, so guardian-only recovery needs no ZK machinery. */ +export type RecoveryModeSpec = + | { kind: 'guardian-only'; guardians: string[]; quorum: number } + | { kind: 'zk-only'; verifier: string; circuitId: string; pool?: string } + | { + kind: 'combined'; + guardians: string[]; + quorum: number; + verifier: string; + circuitId: string; + pool?: string; + }; + +/** Opt-in account-recovery enrollment, camelCase like the other builder + * inputs; {@link PolicyBuilder.recovery} maps it onto perch-ir's + * `RecoveryConfig` wire shape. See `docs/recovery/` for the full design — + * this is reviewable configuration, never the recovery attempt itself. */ +export interface RecoverySpec { + profile: 'loss' | 'protected'; + mode: RecoveryModeSpec; + /** The adopted recovery-controller instance's address (C-strkey). */ + controller: string; + /** Canonical `doc_hash` of the previously-approved baseline document + * suspected-compromise recovery restores. Omit to restrict enrollment to + * lost-key recovery only. */ + baseline?: { docHash: string }; + /** Declared signer ids (`doc.signers[].id`) a recovery attempt may replace. + * Must be non-empty. */ + replaceable: string[]; + delayLedgers: number; + expiryLedgers: number; + maxCancels: number; + /** No default — every enrollment must name this explicitly (mirrors + * perch-ir's `PendingActivityPolicy` having none). */ + pendingActivity: 'freeze' | 'continue'; +} + +function recoveryModeToWire(mode: RecoveryModeSpec): Record { + switch (mode.kind) { + case 'guardian-only': + return { type: 'guardian-only', guardians: mode.guardians, quorum: mode.quorum }; + case 'zk-only': + return { + type: 'zk-only', + verifier: mode.verifier, + 'circuit-id': mode.circuitId, + ...(mode.pool !== undefined ? { pool: mode.pool } : {}), + }; + case 'combined': + return { + type: 'combined', + guardians: mode.guardians, + quorum: mode.quorum, + verifier: mode.verifier, + 'circuit-id': mode.circuitId, + ...(mode.pool !== undefined ? { pool: mode.pool } : {}), + }; + } +} + export class PolicyBuilder { private _network?: string; private readonly _signers: SignerDecl[] = []; private readonly _rules: RuleBuilder[] = []; + private _recovery?: Record; network(name: string): this { this._network = name; @@ -157,6 +222,21 @@ export class PolicyBuilder { this._rules.push(rb); return this; } + /** Enroll opt-in account recovery. See {@link RecoverySpec}. */ + recovery(spec: RecoverySpec): this { + this._recovery = { + profile: spec.profile, + mode: recoveryModeToWire(spec.mode), + controller: spec.controller, + ...(spec.baseline !== undefined ? { baseline: { 'doc-hash': spec.baseline.docHash } } : {}), + replaceable: spec.replaceable, + 'delay-ledgers': spec.delayLedgers, + 'expiry-ledgers': spec.expiryLedgers, + 'max-cancels': spec.maxCancels, + 'pending-activity': spec.pendingActivity, + }; + return this; + } /** Assemble and validate the document (throws on any schema violation). */ build(): PolicyDoc { @@ -166,6 +246,7 @@ export class PolicyBuilder { rules: this._rules.map((r) => r.toWire()), }; if (this._network !== undefined) doc.network = this._network; + if (this._recovery !== undefined) doc.recovery = this._recovery; return parsePolicyDoc(doc); } } diff --git a/packages/perch-js/src/schema.ts b/packages/perch-js/src/schema.ts index ff596af..4597829 100644 --- a/packages/perch-js/src/schema.ts +++ b/packages/perch-js/src/schema.ts @@ -94,12 +94,50 @@ const rule = z }) .strict(); +// Opt-in account-recovery enrollment, mirroring perch-ir's `RecoveryConfig`. +// Reviewable configuration only — never the recovery attempt itself. See +// `docs/recovery/` for the full design and CANONICAL.md for why this field +// being omitted (not `undefined`-but-present) is what keeps recovery-absent +// documents hash-identical to before this field existed. + +const guardianFields = { guardians: z.array(z.string()), quorum: u32 }; +const zkFields = { + verifier: z.string(), + 'circuit-id': z.string(), + pool: z.string().optional(), +}; + +const guardianOnlyMode = z.object({ type: z.literal('guardian-only'), ...guardianFields }).strict(); +const zkOnlyMode = z.object({ type: z.literal('zk-only'), ...zkFields }).strict(); +const combinedMode = z + .object({ type: z.literal('combined'), ...guardianFields, ...zkFields }) + .strict(); +const recoveryMode = z.discriminatedUnion('type', [guardianOnlyMode, zkOnlyMode, combinedMode]); + +const baselineCommitment = z.object({ 'doc-hash': z.string() }).strict(); + +const recoveryConfig = z + .object({ + profile: z.enum(['loss', 'protected']), + mode: recoveryMode, + controller: z.string(), + baseline: baselineCommitment.optional(), + replaceable: z.array(z.string()), + 'delay-ledgers': u32, + 'expiry-ledgers': u32, + 'max-cancels': u32, + // No default — mirrors perch-ir's `PendingActivityPolicy` having none. + 'pending-activity': z.enum(['freeze', 'continue']), + }) + .strict(); + export const policyDocSchema = z .object({ version: z.literal(1), network: z.string().optional(), signers: z.array(signerDecl), rules: z.array(rule), + recovery: recoveryConfig.optional(), }) .strict(); @@ -111,6 +149,9 @@ export type Rule = z.infer; export type ArgConstraint = z.infer; export type ArgPred = z.infer; export type CapConstraint = z.infer; +export type RecoveryConfig = z.infer; +export type RecoveryMode = z.infer; +export type BaselineCommitment = z.infer; /** Parse and validate an already-JSON-parsed value into a PolicyDoc, throwing a * ZodError on any shape/version/unknown-field violation (fail-closed). */ diff --git a/packages/perch-js/test/builder.test.ts b/packages/perch-js/test/builder.test.ts index 92df202..a89ecba 100644 --- a/packages/perch-js/test/builder.test.ts +++ b/packages/perch-js/test/builder.test.ts @@ -72,6 +72,83 @@ describe('fluent builder', () => { expect(doc.rules[0]!.cap).toEqual({ token: REGISTRY, limit: '25', 'period-ledgers': 100 }); }); + it('.recovery() reproduces the ci-publish-recovery fixture (guardian-only) byte-for-byte', () => { + const doc = policy() + .network('Test SDF Network ; September 2015') + .signer('admin', external(WEBAUTHN_VERIFIER, ADMIN_KEY)) + .signer('ci', external(ED25519_VERIFIER, CI_KEY)) + .rule('admin', (r) => r.selfAdmin().signedBy('admin')) + .rule('ci-publish', (r) => + r + .callContract(REGISTRY) + .signedBy('ci') + .func('publish', 'publish_hash') + .arg(1, isSelf()) + .notAfter(55000000), + ) + .recovery({ + profile: 'protected', + mode: { + kind: 'guardian-only', + guardians: [ + 'GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH', + 'GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR', + 'GDWF3WL7PYYUOLQBRMP5V5I62QU73U4KCGN2CLJDW74LPCXWY3YAUVQZ', + ], + quorum: 2, + }, + controller: 'CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF', + baseline: { docHash: '27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98' }, + replaceable: ['admin'], + delayLedgers: 17280, + expiryLedgers: 120960, + maxCancels: 3, + pendingActivity: 'freeze', + }) + .build(); + + expect(docHash(doc)).toBe('dcd539d241eddba9537237f2958a639cd84ac9f1ed6111c18da4230b1b60b08d'); + }); + + it('.recovery() with a combined mode reproduces the ci-publish-recovery-combined fixture', () => { + const doc = policy() + .network('Test SDF Network ; September 2015') + .signer('admin', external(WEBAUTHN_VERIFIER, ADMIN_KEY)) + .signer('ci', external(ED25519_VERIFIER, CI_KEY)) + .rule('admin', (r) => r.selfAdmin().signedBy('admin')) + .rule('ci-publish', (r) => + r + .callContract(REGISTRY) + .signedBy('ci') + .func('publish', 'publish_hash') + .arg(1, isSelf()) + .notAfter(55000000), + ) + .recovery({ + profile: 'loss', + mode: { + kind: 'combined', + guardians: [ + 'GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH', + 'GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR', + ], + quorum: 1, + verifier: 'CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ', + circuitId: '21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb', + pool: 'CDGGTZJDHAPV3S5LD36GAETRHWZ6ASCEZ5YRH7O5JOK3WXW55RRHOLL5', + }, + controller: 'CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF', + replaceable: ['admin'], + delayLedgers: 17280, + expiryLedgers: 120960, + maxCancels: 3, + pendingActivity: 'continue', + }) + .build(); + + expect(docHash(doc)).toBe('9a6c29fdfd28746f8826a945611f31f43e6a51d131ff0bfd018b1ce2762e24bb'); + }); + it('throws on build() when a rule has no scope', () => { expect(() => policy() diff --git a/packages/perch-js/test/parity.test.ts b/packages/perch-js/test/parity.test.ts index 28f020e..57868ad 100644 --- a/packages/perch-js/test/parity.test.ts +++ b/packages/perch-js/test/parity.test.ts @@ -69,3 +69,48 @@ describe('perch-ir parity: ci-publish-threshold fixture', () => { expect(canonicalJson(reparsed)).toBe(canonicalJson(doc)); }); }); + +// The guardian-only recovery variant: a Protected-profile document enrolling +// guardian-only recovery with a baseline pointing at the plain ci-publish +// fixture's own hash. Pins the `recovery` field's canonical form — the schema +// extension this stage adds (see docs/recovery/). +describe('perch-ir parity: ci-publish-recovery fixture (guardian-only)', () => { + const doc = parsePolicyDoc(JSON.parse(readFileSync(td('ci-publish-recovery.json'), 'utf8'))); + + it('canonical JSON is byte-identical to the Rust canonical form', () => { + const committed = readFileSync(td('ci-publish-recovery.canonical.json'), 'utf8').replace( + /\n+$/, + '', + ); + expect(canonicalJson(doc)).toBe(committed); + }); + + it('doc_hash matches the committed and pinned Rust hash', () => { + expect(docHash(doc)).toBe(readFileSync(td('ci-publish-recovery.doc-hash'), 'utf8').trim()); + expect(docHash(doc)).toBe('dcd539d241eddba9537237f2958a639cd84ac9f1ed6111c18da4230b1b60b08d'); + }); +}); + +// The combined (guardian + ZK) recovery variant, Loss profile, no baseline +// (lost-key recovery only). Pins the flattened `combined` mode shape and the +// zk-only-style fields (`verifier`, `circuit-id`, `pool`). +describe('perch-ir parity: ci-publish-recovery-combined fixture', () => { + const doc = parsePolicyDoc( + JSON.parse(readFileSync(td('ci-publish-recovery-combined.json'), 'utf8')), + ); + + it('canonical JSON is byte-identical to the Rust canonical form', () => { + const committed = readFileSync( + td('ci-publish-recovery-combined.canonical.json'), + 'utf8', + ).replace(/\n+$/, ''); + expect(canonicalJson(doc)).toBe(committed); + }); + + it('doc_hash matches the committed and pinned Rust hash', () => { + expect(docHash(doc)).toBe( + readFileSync(td('ci-publish-recovery-combined.doc-hash'), 'utf8').trim(), + ); + expect(docHash(doc)).toBe('9a6c29fdfd28746f8826a945611f31f43e6a51d131ff0bfd018b1ce2762e24bb'); + }); +}); diff --git a/packages/perch-js/test/schema.test.ts b/packages/perch-js/test/schema.test.ts index 85b61b6..e20bb3d 100644 --- a/packages/perch-js/test/schema.test.ts +++ b/packages/perch-js/test/schema.test.ts @@ -69,3 +69,89 @@ describe('fail-closed schema', () => { ).toThrow(); }); }); + +describe('recovery configuration', () => { + const guardianOnlyRecovery = { + profile: 'loss' as const, + mode: { + type: 'guardian-only' as const, + guardians: ['GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH'], + quorum: 1, + }, + controller: 'CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF', + replaceable: ['a'], + 'delay-ledgers': 100, + 'expiry-ledgers': 1000, + 'max-cancels': 3, + 'pending-activity': 'continue' as const, + }; + + it('accepts a document with no `recovery` field at all (the common case)', () => { + expect(() => parsePolicyDoc(valid)).not.toThrow(); + }); + + it('accepts a valid guardian-only recovery config', () => { + expect(() => parsePolicyDoc({ ...valid, recovery: guardianOnlyRecovery })).not.toThrow(); + }); + + it('rejects an unknown field on the recovery object', () => { + expect(() => + parsePolicyDoc({ ...valid, recovery: { ...guardianOnlyRecovery, bogus: 1 } }), + ).toThrow(); + }); + + it('rejects guardian-only mode carrying a ZK field (strict per-variant shape)', () => { + expect(() => + parsePolicyDoc({ + ...valid, + recovery: { + ...guardianOnlyRecovery, + mode: { ...guardianOnlyRecovery.mode, verifier: 'C...' }, + }, + }), + ).toThrow(); + }); + + it('rejects an unknown recovery mode type', () => { + expect(() => + parsePolicyDoc({ + ...valid, + recovery: { ...guardianOnlyRecovery, mode: { type: 'nope' } }, + }), + ).toThrow(); + }); + + it('requires `pending-activity` explicitly (no default)', () => { + const { 'pending-activity': _omit, ...withoutPendingActivity } = guardianOnlyRecovery; + expect(() => parsePolicyDoc({ ...valid, recovery: withoutPendingActivity })).toThrow(); + }); + + it('rejects an invalid `pending-activity` value', () => { + expect(() => + parsePolicyDoc({ + ...valid, + recovery: { ...guardianOnlyRecovery, 'pending-activity': 'restrict' }, + }), + ).toThrow(); + }); + + it('accepts a combined-mode recovery config with baseline', () => { + expect(() => + parsePolicyDoc({ + ...valid, + recovery: { + ...guardianOnlyRecovery, + profile: 'protected', + mode: { + type: 'combined', + guardians: guardianOnlyRecovery.mode.guardians, + quorum: 1, + verifier: 'CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ', + 'circuit-id': '21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb', + }, + baseline: { 'doc-hash': '27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98' }, + }, + }), + ).not.toThrow(); + }); +}); diff --git a/testdata/README.md b/testdata/README.md index 041bcf3..88c7631 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -7,7 +7,10 @@ Golden vectors shared by the Rust and TypeScript test suites: the same doc with the ci signer as a CAP-0071 delegated address, pinning the delegated signer shape; `ci-publish-threshold.*`, the same doc with the publish rule as a 1-of-2 `threshold` quorum carrying a `cap`, pinning those - two rule shapes) + two rule shapes; `ci-publish-recovery.*`, the same doc enrolling + guardian-only `protected` recovery with a baseline; `ci-publish-recovery-combined.*`, + the same doc enrolling combined guardian+ZK `loss` recovery with no + baseline (lost-key only) — see [`../docs/recovery/schema.md`](../docs/recovery/schema.md)) - compiled plans (expected XDR, byte-exact) - constraint program encodings, three-way checked: compiler-built structural `ScVal` == `#[contracttype]` encoding from a test `Env` == TS serializer bytes diff --git a/testdata/ci-publish-recovery-combined.canonical.json b/testdata/ci-publish-recovery-combined.canonical.json new file mode 100644 index 0000000..3935c2d --- /dev/null +++ b/testdata/ci-publish-recovery-combined.canonical.json @@ -0,0 +1 @@ +{"network":"Test SDF Network ; September 2015","recovery":{"controller":"CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF","delay-ledgers":17280,"expiry-ledgers":120960,"max-cancels":3,"mode":{"circuit-id":"21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb","guardians":["GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH","GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR"],"pool":"CDGGTZJDHAPV3S5LD36GAETRHWZ6ASCEZ5YRH7O5JOK3WXW55RRHOLL5","quorum":1,"type":"combined","verifier":"CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ"},"pending-activity":"continue","profile":"loss","replaceable":["admin"]},"rules":[{"name":"admin","principals":{"signers":["admin"],"type":"all"},"scope":{"type":"self-admin"}},{"args":[{"index":1,"pred":{"type":"is-self"}}],"functions":["publish","publish_hash"],"name":"ci-publish","not-after-ledger":55000000,"principals":{"signers":["ci"],"type":"all"},"scope":{"address":"CCA7QAA6OD6LQJTU2MKN6EAS5I52QIFPAYMMQYSU7KHWTGT26AN6N2AL","type":"contract"}}],"signers":[{"id":"admin","key":"045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e","verifier":"CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN"},{"id":"ci","key":"1ce6040b0d03232ac6c911b0c375f1a52ebdefff56fd361d13680e23ca578a17","verifier":"CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG"}],"version":1} diff --git a/testdata/ci-publish-recovery-combined.doc-hash b/testdata/ci-publish-recovery-combined.doc-hash new file mode 100644 index 0000000..0cf53c1 --- /dev/null +++ b/testdata/ci-publish-recovery-combined.doc-hash @@ -0,0 +1 @@ +9a6c29fdfd28746f8826a945611f31f43e6a51d131ff0bfd018b1ce2762e24bb diff --git a/testdata/ci-publish-recovery-combined.json b/testdata/ci-publish-recovery-combined.json new file mode 100644 index 0000000..42b1af1 --- /dev/null +++ b/testdata/ci-publish-recovery-combined.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "network": "Test SDF Network ; September 2015", + "signers": [ + { + "id": "admin", + "verifier": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + }, + { + "id": "ci", + "verifier": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "1ce6040b0d03232ac6c911b0c375f1a52ebdefff56fd361d13680e23ca578a17" + } + ], + "rules": [ + { + "name": "admin", + "scope": { "type": "self-admin" }, + "principals": { "type": "all", "signers": ["admin"] } + }, + { + "name": "ci-publish", + "scope": { + "type": "contract", + "address": "CCA7QAA6OD6LQJTU2MKN6EAS5I52QIFPAYMMQYSU7KHWTGT26AN6N2AL" + }, + "principals": { "type": "all", "signers": ["ci"] }, + "functions": ["publish", "publish_hash"], + "args": [ + { "index": 1, "pred": { "type": "is-self" } } + ], + "not-after-ledger": 55000000 + } + ], + "recovery": { + "profile": "loss", + "mode": { + "type": "combined", + "guardians": [ + "GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH", + "GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR" + ], + "quorum": 1, + "verifier": "CBIRQ266AYZMRM4XFEUR4CHXLIZVHSCK7HWX674P37V4BLTREEH35OHZ", + "circuit-id": "21d53d237ccdb61c57f0b128d9efaf6d96b844b224c2c19a973eaf7b5ee18bbb", + "pool": "CDGGTZJDHAPV3S5LD36GAETRHWZ6ASCEZ5YRH7O5JOK3WXW55RRHOLL5" + }, + "controller": "CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF", + "replaceable": ["admin"], + "delay-ledgers": 17280, + "expiry-ledgers": 120960, + "max-cancels": 3, + "pending-activity": "continue" + } +} diff --git a/testdata/ci-publish-recovery.canonical.json b/testdata/ci-publish-recovery.canonical.json new file mode 100644 index 0000000..b0a1265 --- /dev/null +++ b/testdata/ci-publish-recovery.canonical.json @@ -0,0 +1 @@ +{"network":"Test SDF Network ; September 2015","recovery":{"baseline":{"doc-hash":"27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98"},"controller":"CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF","delay-ledgers":17280,"expiry-ledgers":120960,"max-cancels":3,"mode":{"guardians":["GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH","GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR","GDWF3WL7PYYUOLQBRMP5V5I62QU73U4KCGN2CLJDW74LPCXWY3YAUVQZ"],"quorum":2,"type":"guardian-only"},"pending-activity":"freeze","profile":"protected","replaceable":["admin"]},"rules":[{"name":"admin","principals":{"signers":["admin"],"type":"all"},"scope":{"type":"self-admin"}},{"args":[{"index":1,"pred":{"type":"is-self"}}],"functions":["publish","publish_hash"],"name":"ci-publish","not-after-ledger":55000000,"principals":{"signers":["ci"],"type":"all"},"scope":{"address":"CCA7QAA6OD6LQJTU2MKN6EAS5I52QIFPAYMMQYSU7KHWTGT26AN6N2AL","type":"contract"}}],"signers":[{"id":"admin","key":"045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e","verifier":"CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN"},{"id":"ci","key":"1ce6040b0d03232ac6c911b0c375f1a52ebdefff56fd361d13680e23ca578a17","verifier":"CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG"}],"version":1} diff --git a/testdata/ci-publish-recovery.doc-hash b/testdata/ci-publish-recovery.doc-hash new file mode 100644 index 0000000..2c11337 --- /dev/null +++ b/testdata/ci-publish-recovery.doc-hash @@ -0,0 +1 @@ +dcd539d241eddba9537237f2958a639cd84ac9f1ed6111c18da4230b1b60b08d diff --git a/testdata/ci-publish-recovery.json b/testdata/ci-publish-recovery.json new file mode 100644 index 0000000..349bfa3 --- /dev/null +++ b/testdata/ci-publish-recovery.json @@ -0,0 +1,57 @@ +{ + "version": 1, + "network": "Test SDF Network ; September 2015", + "signers": [ + { + "id": "admin", + "verifier": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + }, + { + "id": "ci", + "verifier": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "1ce6040b0d03232ac6c911b0c375f1a52ebdefff56fd361d13680e23ca578a17" + } + ], + "rules": [ + { + "name": "admin", + "scope": { "type": "self-admin" }, + "principals": { "type": "all", "signers": ["admin"] } + }, + { + "name": "ci-publish", + "scope": { + "type": "contract", + "address": "CCA7QAA6OD6LQJTU2MKN6EAS5I52QIFPAYMMQYSU7KHWTGT26AN6N2AL" + }, + "principals": { "type": "all", "signers": ["ci"] }, + "functions": ["publish", "publish_hash"], + "args": [ + { "index": 1, "pred": { "type": "is-self" } } + ], + "not-after-ledger": 55000000 + } + ], + "recovery": { + "profile": "protected", + "mode": { + "type": "guardian-only", + "guardians": [ + "GALZMP2YGMVP6N57D2E6YVKMK3AONEOSC3F2RAXPOAKRIQVTSHJOOBVH", + "GASP3KHU7JDP23WQINN5DDWC6BYMJ4MFBRLXBPGRAS3EX726YY67JISR", + "GDWF3WL7PYYUOLQBRMP5V5I62QU73U4KCGN2CLJDW74LPCXWY3YAUVQZ" + ], + "quorum": 2 + }, + "controller": "CC5QACNC45UM2FLTKPXD2TME7647YHUPF4PGHQBFRP26PHHDQ6LWAPBF", + "baseline": { + "doc-hash": "27cb38ef07bd8e4f86f07bef4d9272c070c2d9f05063d4c1ad1d4769b1d74a98" + }, + "replaceable": ["admin"], + "delay-ledgers": 17280, + "expiry-ledgers": 120960, + "max-cancels": 3, + "pending-activity": "freeze" + } +} From 59dd833ed838b64cbb78406c1506337a828a0f7c Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 12:22:18 -0400 Subject: [PATCH 2/7] no-mistakes(review): fix(recovery): AND-gate Combined cancellation, keep completed nullifiers spent --- ...ttempts_nullifier_is_never_released.1.json | 1066 ++++++++++++++ ...ardian_quorum_alone_does_not_cancel.1.json | 1234 +++++++++++++++++ ...tion_zk_proof_alone_does_not_cancel.1.json | 1234 +++++++++++++++++ ...ate_domain_from_initiation_approval.1.json | 3 + ...cancellation_requires_a_valid_proof.1.json | 1083 +++++++++++++++ crates/integration-tests/tests/recovery.rs | 224 ++- crates/perch-recovery/src/contract.rs | 68 +- crates/perch-recovery/src/storage.rs | 6 + docs/recovery/controller-governance.md | 14 +- 9 files changed, 4918 insertions(+), 14 deletions(-) create mode 100644 crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json create mode 100644 crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json create mode 100644 crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json create mode 100644 crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json diff --git a/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json new file mode 100644 index 0000000..06159ef --- /dev/null +++ b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json @@ -0,0 +1,1066 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a20227a6b2d6f6e6c79222c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "dba4dd953a5a3a74934b5b64aeadfa34c19c5e3c0b41911e936aa9cae0edc978" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "1" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "469eff78a84a6666bf266df3f2a4150538dfb52df555a554b05416df84cb2609" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "ZkOnly" + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "2" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0505050505050505050505050505050505050505050505050505050505050505" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Revoked" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json new file mode 100644 index 0000000..e15d7cc --- /dev/null +++ b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json @@ -0,0 +1,1234 @@ +{ + "generators": { + "address": 6, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022636f6d62696e6564222c2022677561726469616e73223a205b22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414d445234225d2c202271756f72756d223a20322c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "5f9b4e09bbfd1ef6dbf8e512fcb1df34b35ae0bc9837a90aec541e094a3ec4f8" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Cancelled" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "34a04005bcaf206eec990bd9637d9fdb6725e0a0c0d4aebf003f17f4c956eb5c" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelTally" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelsUsed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "Combined" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0303030303030303030303030303030303030303030303030303030303030303" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "ZkCancelVerified" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json new file mode 100644 index 0000000..9062f91 --- /dev/null +++ b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json @@ -0,0 +1,1234 @@ +{ + "generators": { + "address": 6, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022636f6d62696e6564222c2022677561726469616e73223a205b22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414d445234225d2c202271756f72756d223a20322c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_cancel", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "5f9b4e09bbfd1ef6dbf8e512fcb1df34b35ae0bc9837a90aec541e094a3ec4f8" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Cancelled" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "34a04005bcaf206eec990bd9637d9fdb6725e0a0c0d4aebf003f17f4c956eb5c" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelTally" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelsUsed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "Combined" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0404040404040404040404040404040404040404040404040404040404040404" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "ZkCancelVerified" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json index 0aaccaa..257ade0 100644 --- a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json +++ b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json @@ -736,6 +736,9 @@ "vec": [ { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" } ] } diff --git a/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json new file mode 100644 index 0000000..3c78420 --- /dev/null +++ b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json @@ -0,0 +1,1083 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a20227a6b2d6f6e6c79222c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "dba4dd953a5a3a74934b5b64aeadfa34c19c5e3c0b41911e936aa9cae0edc978" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Cancelled" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "34a04005bcaf206eec990bd9637d9fdb6725e0a0c0d4aebf003f17f4c956eb5c" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "CancelsUsed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "ZkOnly" + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0202020202020202020202020202020202020202020202020202020202020202" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "ZkCancelVerified" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs index 88402e3..87c5a9f 100644 --- a/crates/integration-tests/tests/recovery.rs +++ b/crates/integration-tests/tests/recovery.rs @@ -20,7 +20,10 @@ use perch_recovery::{PerchRecovery, PerchRecoveryClient}; use perch_testkit::{no_recovery_evidence, Bootstrap, World, FIXTURE_NETWORK}; use soroban_sdk::auth::{Context, ContractContext}; use soroban_sdk::testutils::{Address as _, Ledger}; -use soroban_sdk::{crypto::Hash, vec, Address, Bytes, BytesN, IntoVal, Map, Symbol}; +use soroban_sdk::{ + contract, contractimpl, crypto::Hash, vec, Address, Bytes, BytesN, Env, IntoVal, Map, Symbol, + Vec, +}; use stellar_accounts::smart_account::{do_check_auth, AuthPayload}; const ADMIN_VERIFIER: &str = "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN"; @@ -84,6 +87,80 @@ fn target_doc(controller: &Address, guardians: &[Address], quorum: u32) -> std:: ) } +/// A minimal `ZkVerifierInterface`-shaped mock: no real circuit, per +/// `docs/recovery/controller-governance.md`'s "ZK adapter scope". Validity is +/// controlled purely by whether the caller passed a non-empty `proof`, so +/// tests can exercise the controller's own evidence-tracking/gating logic +/// (which factors it requires, and when) without a real verifier. +#[contract] +struct MockZkVerifier; + +#[contractimpl] +impl MockZkVerifier { + pub fn verify_proof( + _e: &Env, + _statement: BytesN<32>, + _nullifier: BytesN<32>, + proof: Bytes, + _pool: Vec
, + ) -> bool { + proof.len() > 0 + } +} + +/// Same document shape as `enroll_doc`/`target_doc`, but with an arbitrary +/// recovery `mode` block — for zk-only/combined-mode tests that don't need +/// guardian-only's specific JSON. +fn enroll_doc_with_mode(controller: &Address, mode_json: &str) -> std::string::String { + format!( + r#"{{ + "version": 1, + "network": "{FIXTURE_NETWORK}", + "signers": [ + {{ "id": "admin", "verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}" }} + ], + "rules": [ + {{ "name": "admin", "scope": {{ "type": "self-admin" }}, + "principals": {{ "type": "all", "signers": ["admin"] }} }} + ], + "recovery": {{ + "profile": "loss", + "mode": {mode_json}, + "controller": "{controller}", + "replaceable": ["admin"], + "delay-ledgers": 5, + "expiry-ledgers": 1000, + "max-cancels": 3, + "pending-activity": "continue" + }} +}}"#, + controller = strkey(controller), + ) +} + +fn zk_only_mode_json(verifier: &Address) -> std::string::String { + format!( + r#"{{ "type": "zk-only", "verifier": "{}", "circuit-id": "ab" }}"#, + strkey(verifier) + ) +} + +fn combined_mode_json( + verifier: &Address, + guardians: &[Address], + quorum: u32, +) -> std::string::String { + let guardian_list = guardians + .iter() + .map(|g| format!("\"{}\"", strkey(g))) + .collect::>() + .join(","); + format!( + r#"{{ "type": "combined", "guardians": [{guardian_list}], "quorum": {quorum}, "verifier": "{}", "circuit-id": "ab" }}"#, + strkey(verifier) + ) +} + fn recovery_rule_id(w: &World) -> u32 { let client = w.account_client(); let n = client.get_context_rules_count(); @@ -357,3 +434,148 @@ fn protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused() { let recovery = PerchRecoveryClient::new(&w.env, &controller); assert!(recovery.get_config(&w.account).is_some()); } + +#[test] +fn zk_only_cancellation_requires_a_valid_proof() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + + let doc = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target_hash: BytesN<32> = w + .env + .crypto() + .sha256(&Bytes::from_slice(&w.env, b"target")) + .to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + let invalid_proof = Bytes::new(&w.env); + let nullifier1 = BytesN::from_array(&w.env, &[1u8; 32]); + assert!(recovery + .try_submit_zk_cancel(&w.account, &nullifier1, &invalid_proof) + .is_err()); + assert!(recovery.has_pending(&w.account)); + + let valid_proof = Bytes::from_array(&w.env, &[1u8; 1]); + let nullifier2 = BytesN::from_array(&w.env, &[2u8; 32]); + recovery.submit_zk_cancel(&w.account, &nullifier2, &valid_proof); + assert!(!recovery.has_pending(&w.account)); +} + +#[test] +fn combined_cancellation_guardian_quorum_alone_does_not_cancel() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + let doc = enroll_doc_with_mode( + &controller, + &combined_mode_json(&verifier, &[g1.clone(), g2.clone()], 2), + ); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target_hash: BytesN<32> = w + .env + .crypto() + .sha256(&Bytes::from_slice(&w.env, b"target")) + .to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + // Guardian quorum alone does not cancel a `Combined`-mode attempt. + recovery.submit_guardian_cancel(&w.account, &g1); + recovery.submit_guardian_cancel(&w.account, &g2); + assert!(recovery.has_pending(&w.account)); + + // The ZK factor completes the requirement. + let valid_proof = Bytes::from_array(&w.env, &[1u8; 1]); + let nullifier = BytesN::from_array(&w.env, &[3u8; 32]); + recovery.submit_zk_cancel(&w.account, &nullifier, &valid_proof); + assert!(!recovery.has_pending(&w.account)); +} + +#[test] +fn combined_cancellation_zk_proof_alone_does_not_cancel() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + let doc = enroll_doc_with_mode( + &controller, + &combined_mode_json(&verifier, &[g1.clone(), g2.clone()], 2), + ); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target_hash: BytesN<32> = w + .env + .crypto() + .sha256(&Bytes::from_slice(&w.env, b"target")) + .to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + // A ZK cancellation proof alone does not cancel a `Combined`-mode attempt. + let valid_proof = Bytes::from_array(&w.env, &[1u8; 1]); + let nullifier = BytesN::from_array(&w.env, &[4u8; 32]); + recovery.submit_zk_cancel(&w.account, &nullifier, &valid_proof); + assert!(recovery.has_pending(&w.account)); + + // The guardian factor completes the requirement. + recovery.submit_guardian_cancel(&w.account, &g1); + recovery.submit_guardian_cancel(&w.account, &g2); + assert!(!recovery.has_pending(&w.account)); +} + +#[test] +fn a_completed_attempts_nullifier_is_never_released() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + + let doc = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let rule_id = recovery_rule_id(&w); + let target = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)).replace( + &format!(r#""verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}""#), + &format!(r#""verifier": "{NEW_ADMIN_VERIFIER}", "key": "{NEW_ADMIN_KEY}""#), + ); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + let proof = Bytes::from_array(&w.env, &[1u8; 1]); + let nullifier = BytesN::from_array(&w.env, &[5u8; 32]); + recovery.submit_zk_proof(&w.account, &nullifier, &proof); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_ok()); + + // A fresh attempt after completion must not resurrect the spent nullifier. + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + assert!(recovery + .try_submit_zk_proof(&w.account, &nullifier, &proof) + .is_err()); +} diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs index 002c28b..7d594a6 100644 --- a/crates/perch-recovery/src/contract.rs +++ b/crates/perch-recovery/src/contract.rs @@ -370,6 +370,10 @@ impl PerchRecovery { /// A guardian approves cancellation of this account's identified attempt. /// Own action domain — initiation approvals never count here (§2.2). + /// Only actually cancels once the mode's full cancellation evidence set + /// is present: for `Combined`, reaching guardian quorum here is not + /// enough by itself — a verified ZK cancellation proof is also required + /// (see `cancellation_satisfied`). pub fn submit_guardian_cancel( e: &Env, account: Address, @@ -388,18 +392,24 @@ impl PerchRecovery { return Err(RecoveryError::AlreadyApproved); } tally.push_back(guardian); - if tally.len() >= g.quorum { - cancel_attempt(e, &account, &mut attempt)?; - } else { - RecoveryStorage::set_cancel_tally(e, &key, &tally); - RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::set_cancel_tally(e, &key, &tally); + RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + let guardian_quorum_reached = tally.len() >= g.quorum; + if guardian_quorum_reached { + let zk_cancel_verified = RecoveryStorage::get_zk_cancel_verified(e, &key).unwrap_or(false); + if cancellation_satisfied(&config.mode, true, zk_cancel_verified) { + cancel_attempt(e, &account, &mut attempt)?; + } } Ok(()) } /// Submit a ZK cancellation proof for this account's identified attempt. /// Own action domain (`Action::Cancel`) — an initiation proof never - /// satisfies this, and vice versa. + /// satisfies this, and vice versa. Only actually cancels once the mode's + /// full cancellation evidence set is present: for `Combined`, a valid + /// proof here is not enough by itself — a guardian quorum on this same + /// attempt is also required (see `cancellation_satisfied`). pub fn submit_zk_cancel( e: &Env, account: Address, @@ -426,8 +436,22 @@ impl PerchRecovery { if !ZkVerifierClient::new(e, &z.verifier).verify_proof(&stmt, &nullifier, &proof, &z.pool) { return Err(RecoveryError::ZkProofInvalid); } - cancel_attempt(e, &account, &mut attempt)?; RecoveryStorage::set_nullifier(e, &nullifier, &true); + let key = (account.clone(), attempt.id); + RecoveryStorage::set_zk_cancel_verified(e, &key, &true); + RecoveryStorage::extend_zk_cancel_verified_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + let guardian_quorum_reached = match guardian_set(&config.mode) { + Some(g) => { + RecoveryStorage::get_cancel_tally(e, &key) + .unwrap_or_else(|| Vec::new(e)) + .len() + >= g.quorum + } + None => false, + }; + if cancellation_satisfied(&config.mode, guardian_quorum_reached, true) { + cancel_attempt(e, &account, &mut attempt)?; + } Ok(()) } @@ -515,6 +539,22 @@ fn initiation_satisfied(mode: &CompiledRecoveryMode, attempt: &Attempt) -> bool } } +/// Mirrors `initiation_satisfied` for the cancellation domain (§2.2): +/// `GuardianOnly`/`ZkOnly` need only their own factor, `Combined` needs both +/// a guardian quorum AND a valid ZK cancellation proof for the same attempt — +/// neither factor alone may cancel a `Combined`-mode attempt. +fn cancellation_satisfied( + mode: &CompiledRecoveryMode, + guardian_quorum_reached: bool, + zk_cancel_verified: bool, +) -> bool { + match mode { + CompiledRecoveryMode::GuardianOnly(_) => guardian_quorum_reached, + CompiledRecoveryMode::ZkOnly(_) => zk_cancel_verified, + CompiledRecoveryMode::Combined(_, _) => guardian_quorum_reached && zk_cancel_verified, + } +} + fn maybe_promote(e: &Env, config: &CompiledRecoveryConfig, attempt: &mut Attempt) { if attempt.state == AttemptState::CollectingEvidence && initiation_satisfied(&config.mode, attempt) @@ -550,10 +590,16 @@ fn begin_attempt( if is_live(e, &existing) { return Err(RecoveryError::AttemptPending); } - // Stale (terminal or expired): release its nullifier before - // replacing it, so the same secret can be used again. - if let Some(n) = existing.nullifier.first() { - RecoveryStorage::set_nullifier(e, &n, &false); + // Stale (terminal or expired), and not a completed attempt: release + // its nullifier before replacing it, so the same secret can be used + // again. A `Completed` attempt's nullifier stays spent permanently + // (see `Attempt::nullifier`'s documented invariant) — completion + // itself already extends the nullifier's own TTL as the durable + // record of that. + if existing.state != AttemptState::Completed { + if let Some(n) = existing.nullifier.first() { + RecoveryStorage::set_nullifier(e, &n, &false); + } } } let id = RecoveryStorage::get_next_attempt_id(e, &account).unwrap_or(0); diff --git a/crates/perch-recovery/src/storage.rs b/crates/perch-recovery/src/storage.rs index 572e3c1..83d4b7c 100644 --- a/crates/perch-recovery/src/storage.rs +++ b/crates/perch-recovery/src/storage.rs @@ -28,6 +28,12 @@ pub struct RecoveryStorage { /// deliberately separate domain from `Attempt::guardian_approvals` /// (initiation evidence never counts toward cancellation, per §2.2). pub cancel_tally: PersistentMap<(Address, u64), Vec
>, + /// Whether a valid ZK cancellation proof has been verified for one + /// `(account, attempt_id)`. Tracked separately from `cancel_tally` so + /// `Combined` mode can require both factors before cancelling (§2.2) — + /// each factor's own evidence is recorded independently and + /// `cancel_attempt` only fires once the mode's full set is present. + pub zk_cancel_verified: PersistentMap<(Address, u64), bool>, /// Permanent, append-only fingerprints of every credential this account /// has ever had recovery-revoked. A later attempt — even from an old /// baseline — must never reintroduce one of these. diff --git a/docs/recovery/controller-governance.md b/docs/recovery/controller-governance.md index a250df8..5cca02f 100644 --- a/docs/recovery/controller-governance.md +++ b/docs/recovery/controller-governance.md @@ -180,8 +180,11 @@ own document — never anything at the existing address. ## Adversarial properties proven end-to-end -`crates/integration-tests/tests/recovery.rs` (guardian-only mode, driving -the real `do_check_auth`/`Policy::enforce` path): +`crates/integration-tests/tests/recovery.rs` (guardian-only mode for +completion, driving the real `do_check_auth`/`Policy::enforce` path; +guardian-only/ZK-only/`Combined` for cancellation, the latter two against a +mock verifier — see "ZK adapter scope" for what that does and doesn't +prove): - No attempt at all → completion refused. - Below guardian quorum → completion refused, and does not promote. @@ -195,7 +198,14 @@ the real `do_check_auth`/`Policy::enforce` path): only once quorum is actually reached. - Cancellation evidence is a separate domain from initiation evidence — an initiation approval does not count toward cancelling the same attempt. +- `Combined`-mode cancellation requires BOTH a guardian quorum AND a valid ZK + cancellation proof for the same attempt — guardian quorum alone does not + cancel, a ZK proof alone does not cancel, and only once both factors are + present does the attempt cancel (§2.2). - A cancelled attempt allows a fresh one afterward. +- A completed attempt's nullifier is never released back to unspent by a + later `begin_*_attempt` call — only a cancelled or expired attempt's + nullifier is released. - A `Protected` reconfiguration (here: disabling recovery entirely) with ordinary admin authorization alone is refused. From 48d9521989e77746e332c821f311bffb38ad100e Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 12:28:02 -0400 Subject: [PATCH 3/7] no-mistakes(review): fix(recovery): reject cancellation evidence on non-live attempts --- ...used_once_the_attempt_has_completed.1.json | 1056 ++++++++++++++++ ...used_once_the_attempt_has_completed.1.json | 1070 +++++++++++++++++ crates/integration-tests/tests/recovery.rs | 77 ++ crates/perch-recovery/src/contract.rs | 6 + docs/recovery/controller-governance.md | 3 + 5 files changed, 2212 insertions(+) create mode 100644 crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json create mode 100644 crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json diff --git a/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json new file mode 100644 index 0000000..96cc8b9 --- /dev/null +++ b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -0,0 +1,1056 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134225d2c202271756f72756d223a2031207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "submit_guardian_approval", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "4f9d139a0335a5591a951e4d24a35e55466e21072c8fe20781cb73a9af637b21" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Completed" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "6d63e6dc9f001bb99617560fc90d95e59760720b829cf3d1365de9aa106509c2" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 1 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Revoked" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json new file mode 100644 index 0000000..f029e1d --- /dev/null +++ b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -0,0 +1,1070 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a20227a6b2d6f6e6c79222c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 5, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "dba4dd953a5a3a74934b5b64aeadfa34c19c5e3c0b41911e936aa9cae0edc978" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [ + { + "bytes": "0606060606060606060606060606060606060606060606060606060606060606" + } + ] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "Completed" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "469eff78a84a6666bf266df3f2a4150538dfb52df555a554b05416df84cb2609" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": true + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "ZkOnly" + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0606060606060606060606060606060606060606060606060606060606060606" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Revoked" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4100 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs index 87c5a9f..2171edf 100644 --- a/crates/integration-tests/tests/recovery.rs +++ b/crates/integration-tests/tests/recovery.rs @@ -579,3 +579,80 @@ fn a_completed_attempts_nullifier_is_never_released() { .try_submit_zk_proof(&w.account, &nullifier, &proof) .is_err()); } + +#[test] +fn guardian_cancellation_is_refused_once_the_attempt_has_completed() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + + let doc = enroll_doc(&controller, &[g1.clone()], 1); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let rule_id = recovery_rule_id(&w); + let target = target_doc(&controller, &[g1.clone()], 1); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + recovery.submit_guardian_approval(&w.account, &g1); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_ok()); + + // A cancellation factor arriving after completion must not flip the + // attempt back to `Cancelled`. + assert!(recovery + .try_submit_guardian_cancel(&w.account, &g1) + .is_err()); + assert_eq!( + recovery.get_attempt(&w.account).unwrap().state, + perch_recovery::types::AttemptState::Completed + ); +} + +#[test] +fn zk_cancellation_is_refused_once_the_attempt_has_completed() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + + let doc = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let rule_id = recovery_rule_id(&w); + let target = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)).replace( + &format!(r#""verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}""#), + &format!(r#""verifier": "{NEW_ADMIN_VERIFIER}", "key": "{NEW_ADMIN_KEY}""#), + ); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + let proof = Bytes::from_array(&w.env, &[1u8; 1]); + let init_nullifier = BytesN::from_array(&w.env, &[6u8; 32]); + recovery.submit_zk_proof(&w.account, &init_nullifier, &proof); + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.executable_after); + assert!(complete_via_rule(&w, &w.account, rule_id, &target_bytes).is_ok()); + + let cancel_nullifier = BytesN::from_array(&w.env, &[7u8; 32]); + assert!(recovery + .try_submit_zk_cancel(&w.account, &cancel_nullifier, &proof) + .is_err()); + assert_eq!( + recovery.get_attempt(&w.account).unwrap().state, + perch_recovery::types::AttemptState::Completed + ); +} diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs index 7d594a6..27561e5 100644 --- a/crates/perch-recovery/src/contract.rs +++ b/crates/perch-recovery/src/contract.rs @@ -386,6 +386,9 @@ impl PerchRecovery { return Err(RecoveryError::NotAGuardian); } let mut attempt = require_attempt(e, &account)?; + if !is_live(e, &attempt) { + return Err(RecoveryError::NoLiveAttempt); + } let key = (account.clone(), attempt.id); let mut tally = RecoveryStorage::get_cancel_tally(e, &key).unwrap_or_else(|| Vec::new(e)); if tally.contains(&guardian) { @@ -419,6 +422,9 @@ impl PerchRecovery { let config = require_config(e, &account)?; let z = zk_config(&config.mode).ok_or(RecoveryError::ModeHasNoZk)?; let mut attempt = require_attempt(e, &account)?; + if !is_live(e, &attempt) { + return Err(RecoveryError::NoLiveAttempt); + } if RecoveryStorage::get_nullifier(e, &nullifier).unwrap_or(false) { return Err(RecoveryError::NullifierAlreadySpent); } diff --git a/docs/recovery/controller-governance.md b/docs/recovery/controller-governance.md index 5cca02f..c610147 100644 --- a/docs/recovery/controller-governance.md +++ b/docs/recovery/controller-governance.md @@ -203,6 +203,9 @@ prove): cancel, a ZK proof alone does not cancel, and only once both factors are present does the attempt cancel (§2.2). - A cancelled attempt allows a fresh one afterward. +- A cancellation factor (guardian or ZK) arriving after an attempt has + already completed is refused, not silently accepted — a completed attempt + can never be flipped back to `Cancelled`. - A completed attempt's nullifier is never released back to unspent by a later `begin_*_attempt` call — only a cancelled or expired attempt's nullifier is released. From 8b1b159985cdb3d05a2b2c89da451ee9c5ebc2b3 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 12:36:07 -0400 Subject: [PATCH 4/7] chore(recovery): fmt + clippy cleanup on top of pipeline fix commits Mechanical only: clippy::len_zero, clippy::cloned_ref_to_slice_refs in the new cancellation tests, and rustfmt on the AND-gate fix in contract.rs. No behavior change; the pipeline's test step (which passed all 15 recovery integration tests plus the broader targeted suite) never reached the lint step because a known no-mistakes test-analyzer bug ("scenario N result 'pass' requires live validation") failed the run first. --- crates/integration-tests/tests/recovery.rs | 6 +++--- crates/perch-recovery/src/contract.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs index 2171edf..1d779cc 100644 --- a/crates/integration-tests/tests/recovery.rs +++ b/crates/integration-tests/tests/recovery.rs @@ -104,7 +104,7 @@ impl MockZkVerifier { proof: Bytes, _pool: Vec
, ) -> bool { - proof.len() > 0 + !proof.is_empty() } } @@ -587,13 +587,13 @@ fn guardian_cancellation_is_refused_once_the_attempt_has_completed() { let recovery = PerchRecoveryClient::new(&w.env, &controller); let g1 = Address::generate(&w.env); - let doc = enroll_doc(&controller, &[g1.clone()], 1); + let doc = enroll_doc(&controller, std::slice::from_ref(&g1), 1); w.account_client().apply_doc( &Bytes::from_slice(&w.env, doc.as_bytes()), &no_recovery_evidence(&w.env), ); let rule_id = recovery_rule_id(&w); - let target = target_doc(&controller, &[g1.clone()], 1); + let target = target_doc(&controller, std::slice::from_ref(&g1), 1); let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); let replaceable = recovery.get_config(&w.account).unwrap().replaceable; diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs index 27561e5..d51988b 100644 --- a/crates/perch-recovery/src/contract.rs +++ b/crates/perch-recovery/src/contract.rs @@ -399,7 +399,8 @@ impl PerchRecovery { RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); let guardian_quorum_reached = tally.len() >= g.quorum; if guardian_quorum_reached { - let zk_cancel_verified = RecoveryStorage::get_zk_cancel_verified(e, &key).unwrap_or(false); + let zk_cancel_verified = + RecoveryStorage::get_zk_cancel_verified(e, &key).unwrap_or(false); if cancellation_satisfied(&config.mode, true, zk_cancel_verified) { cancel_attempt(e, &account, &mut attempt)?; } From 21b0e37ddb65e66e5daabd21737aa2c9d5211ea7 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 17:49:44 -0400 Subject: [PATCH 5/7] fix(recovery): close review-surfaced auth/timing gaps; reframe docs as standalone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold every finding from PR #83's review into the account-recovery feature itself, and drop the internal staged-plan framing from its docs and code comments so the feature is documented and reviewable on its own terms. Security/correctness fixes in crates/perch-recovery and perch-doc-compiler: - `install`, `enforce`, and `guard_apply_doc` are exported contract functions callable directly by anyone, not only via the real OZ/apply_doc flow. Each now starts with `require_auth()` on the relevant account, which succeeds for free on the real path (Soroban invoker-contract authorization) and rejects a direct, forged call — verified against the pinned soroban-env-host's own auth-tracker source. - `submit_guardian_approval`/`submit_guardian_cancel` now authenticate a digest bound to the specific attempt (id, action, target, config) via `require_auth_for_args`, instead of the bare `(account, guardian)` call arguments, so a signature can't be redirected to a different attempt. - `submit_zk_proof` reserves its nullifier immediately after verification instead of deferring to completion, closing a window where the same nullifier could authorize two different statements before either finished. - `begin_compromise_attempt` now requires its target to equal the enrolled baseline exactly, instead of only checking that a baseline exists. - `Attempt` gains `evidence_deadline`, closing a permissionless, no-cost-to-grief path where a `CollectingEvidence` attempt with no evidence ever arriving would block ordinary `apply_doc` calls forever. - `credential_fingerprint` decodes a signer's hex key to its physical bytes before hashing, matching `perch-ir` validation's own casing-insensitive treatment, so re-declaring a revoked credential under different hex casing no longer evades revocation. - `perch-recovery`'s `soroban-sdk-tools` dependency is no longer gated behind the `contract` feature, fixing a client-only build (as `perch-smart-account` uses) that referenced `#[scerr]` unconditionally. - TTL extension across the controller now uses the network's live `max_ttl()` instead of a fixed ~180-day constant, extends on the highest-traffic read paths (not just writes), and a new permissionless `renew` entry point lets a keeper extend an otherwise-idle account's state indefinitely — documented in controller-governance.md as a real, inherited platform constraint rather than solved outright. - Checked arithmetic in the timelock/expiry computation, and the `RecoveryAuthorized` event now names the actual account instead of the controller's own address. Regression tests added in crates/perch-recovery (a `zk::statement` unit test proving the guardian-approval digest actually varies per attempt) and crates/integration-tests/tests/recovery.rs (baseline mismatch, evidence-deadline timeout, nullifier-reservation immediacy, fingerprint hex-casing). Docs: reworded docs/recovery/* and the crate's own doc comments to explain every design decision on its own terms (renamed section-7-gate.md to pending-activity-policy.md, dropped "Stage 4"/"§N" references throughout), added a "What the commitment does, and does not, verify" section making explicit that target-document content correctness relative to `replaceable`/`revoked` is an evidence-provider review responsibility, not on-chain-verified — the same trust boundary a companion smart-account implementation this design generalizes from also accepts. Behavior for existing (non-recovery, and previously-passing recovery) flows is unchanged; full workspace test/fmt/clippy suite and perch-js tests pass. --- Cargo.lock | 1 + ..._once_its_evidence_deadline_elapses.1.json | 1045 +++++++++++++++++ ...ttempts_nullifier_is_never_released.1.json | 16 +- ...t_that_is_not_the_enrolled_baseline.1.json | 978 +++++++++++++++ ...ardian_quorum_alone_does_not_cancel.1.json | 22 +- ...tion_zk_proof_alone_does_not_cancel.1.json | 22 +- .../completion_cannot_be_replayed.1.json | 24 +- ...refused_before_the_timelock_elapses.1.json | 22 +- ...etion_refused_below_guardian_quorum.1.json | 17 +- ...fused_for_the_wrong_target_document.1.json | 22 +- ...ion_refused_with_no_evidence_at_all.1.json | 12 +- ...fingerprint_is_hex_case_insensitive.1.json | 875 ++++++++++++++ ...ate_domain_from_initiation_approval.1.json | 27 +- ...used_once_the_attempt_has_completed.1.json | 19 +- ...es_and_installs_the_target_document.1.json | 24 +- ...ever_promotes_without_being_reached.1.json | 27 +- ...ian_evidence_admin_alone_is_refused.1.json | 2 +- ...used_once_the_attempt_has_completed.1.json | 16 +- ...cancellation_requires_a_valid_proof.1.json | 12 +- ...ediately_not_deferred_to_completion.1.json | 1033 ++++++++++++++++ crates/integration-tests/tests/recovery.rs | 185 +++ crates/perch-doc-compiler/Cargo.toml | 7 +- crates/perch-doc-compiler/src/lib.rs | 36 +- crates/perch-recovery/Cargo.toml | 7 +- crates/perch-recovery/src/contract.rs | 271 ++++- crates/perch-recovery/src/lib.rs | 19 +- crates/perch-recovery/src/storage.rs | 11 +- crates/perch-recovery/src/types.rs | 29 +- crates/perch-recovery/src/zk.rs | 147 ++- docs/recovery/README.md | 43 +- docs/recovery/account-mutation-paths.md | 29 +- docs/recovery/controller-governance.md | 315 +++-- docs/recovery/formal-verification-impact.md | 20 +- docs/recovery/migration.md | 43 +- docs/recovery/pending-activity-policy.md | 94 ++ docs/recovery/schema.md | 113 +- docs/recovery/section-7-gate.md | 81 -- .../vk-and-controller-immutability.md | 47 +- 38 files changed, 5215 insertions(+), 498 deletions(-) create mode 100644 crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json create mode 100644 crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json create mode 100644 crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json create mode 100644 crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json create mode 100644 docs/recovery/pending-activity-policy.md delete mode 100644 docs/recovery/section-7-gate.md diff --git a/Cargo.lock b/Cargo.lock index 746123f..4d4f04f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1470,6 +1470,7 @@ dependencies = [ name = "perch-doc-compiler" version = "0.2.1" dependencies = [ + "hex", "perch-compile", "perch-ir", "perch-program", diff --git a/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json b/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json new file mode 100644 index 0000000..0b7986f --- /dev/null +++ b/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json @@ -0,0 +1,1045 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c22434141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414b33494d225d2c202271756f72756d223a2032207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 1000, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 3 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 2 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 4 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 2 + } + } + }, + "ext": "v0" + }, + "live_until": 5095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "59b02b26bde1c27a8c5cb2bf32818abc985ccec56071cde0c1a3a82a11dab563" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 3 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6312999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 2000 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "1" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "c7a523d3221866b9b6753ade298ddfe37f94e0a689dee3b41a8981540e101ac5" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 2 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "2" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json index 06159ef..16f3a07 100644 --- a/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json +++ b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json @@ -568,6 +568,14 @@ "u32": 5 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1005 + } + }, { "key": { "symbol": "executable_after" @@ -615,7 +623,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -787,7 +795,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -852,7 +860,7 @@ }, "ext": "v0" }, - "live_until": 4100 + "live_until": 4095 }, { "entry": { @@ -875,7 +883,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json b/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json new file mode 100644 index 0000000..7fd91bd --- /dev/null +++ b/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json @@ -0,0 +1,978 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a202270726f746563746564222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134225d2c202271756f72756d223a2031207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a2020202022626173656c696e65223a207b2022646f632d68617368223a20223033303330333033303330333033303330333033303330333033303330333033303330333033303330333033303330333033303330333033303330333033303322207d2c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "4ccad22302a1a9ac0fd0de3ee5a9635c984eb922f33ec2915f0d8a815e897220" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "Compromise" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "CollectingEvidence" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "0303030303030303030303030303030303030303030303030303030303030303" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [ + { + "bytes": "0303030303030303030303030303030303030303030303030303030303030303" + } + ] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 1 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Protected" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json index e15d7cc..a6f744c 100644 --- a/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json +++ b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json @@ -73,10 +73,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" } ] } @@ -95,10 +92,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" } ] } @@ -598,6 +592,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -645,7 +647,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -912,7 +914,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json index 9062f91..279e9a2 100644 --- a/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json +++ b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json @@ -75,10 +75,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" } ] } @@ -97,10 +94,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" } ] } @@ -598,6 +592,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -645,7 +647,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -912,7 +914,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json index d88c074..4263047 100644 --- a/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json +++ b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -106,10 +103,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -608,6 +602,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -662,7 +664,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -833,7 +835,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -894,7 +896,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json index 6772621..00bd237 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -106,10 +103,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -607,6 +601,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -661,7 +663,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -832,7 +834,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json index 715f76c..00110b7 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -585,6 +582,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -636,7 +641,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -807,7 +812,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json index 73256d8..3c21e6b 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -106,10 +103,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -607,6 +601,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -661,7 +663,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -832,7 +834,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json index 160e61f..3c4e3f4 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json @@ -562,6 +562,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -609,7 +617,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -780,7 +788,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json b/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json new file mode 100644 index 0000000..c54c0c6 --- /dev/null +++ b/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json @@ -0,0 +1,875 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134225d2c202271756f72756d223a2031207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034354532413735383942373343313944353334314346313241433043354636433435433239384434433230303032433739344441414441464442383346333546354245323339363336343844374141434346354532373338303346324645433741384630454234443438343543394238394139373242344130393239384231374522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a2022677561726469616e2d6f6e6c79222c2022677561726469616e73223a205b224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134225d2c202271756f72756d223a2031207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 3 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 2 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 4 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 2 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "4410d5719f312dc4dcfb19fc400bcb3ed303894ba73bf7bf75536e0a2b8c358a" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 3 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "GuardianOnly" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "quorum" + }, + "val": { + "u32": 1 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json index 257ade0..c2ef6ea 100644 --- a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json +++ b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json @@ -72,10 +72,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -94,10 +91,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "618e4ee371df04b636c4f597f9854fd7ecc63d67ea963f3cc49ff2f4ba6ee3c6" } ] } @@ -117,10 +111,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "618e4ee371df04b636c4f597f9854fd7ecc63d67ea963f3cc49ff2f4ba6ee3c6" } ] } @@ -619,6 +610,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -666,7 +665,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -905,7 +904,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json index 96cc8b9..17821ed 100644 --- a/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json +++ b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "5c15ca812c2379c45352526f6146e419a6102f13a4bfebae465df84d2b3e1ef5" } ] } @@ -588,6 +585,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -639,7 +644,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -807,7 +812,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -868,7 +873,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json index a8b0b3f..282f08a 100644 --- a/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json +++ b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json @@ -84,10 +84,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -106,10 +103,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" } ] } @@ -610,6 +604,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -664,7 +666,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -835,7 +837,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -896,7 +898,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json index 4e24269..51e6b1d 100644 --- a/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json +++ b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json @@ -72,10 +72,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" } ] } @@ -94,10 +91,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" } ] } @@ -117,10 +111,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" } ] } @@ -618,6 +609,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -675,7 +674,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -849,7 +848,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json index 74144dd..83d6385 100644 --- a/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json +++ b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json @@ -640,7 +640,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json index f029e1d..65e232f 100644 --- a/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json +++ b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -568,6 +568,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -619,7 +627,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -791,7 +799,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -856,7 +864,7 @@ }, "ext": "v0" }, - "live_until": 4100 + "live_until": 4095 }, { "entry": { @@ -879,7 +887,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json index 3c78420..980a6b7 100644 --- a/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json +++ b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json @@ -555,6 +555,14 @@ "u32": 0 } }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, { "key": { "symbol": "executable_after" @@ -602,7 +610,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } @@ -801,7 +809,7 @@ "val": { "vec": [ { - "bytes": "37d1b9bad37145b43830623395939bb877c08c8a2f8e0801c769a963045f7ff1" + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" } ] } diff --git a/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json b/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json new file mode 100644 index 0000000..e13c701 --- /dev/null +++ b/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json @@ -0,0 +1,1033 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "apply_doc", + "args": [ + { + "bytes": "7b0a20202276657273696f6e223a20312c0a2020226e6574776f726b223a20225465737420534446204e6574776f726b203b2053657074656d6265722032303135222c0a2020227369676e657273223a205b0a202020207b20226964223a202261646d696e222c20227665726966696572223a202243443449463735444e514a4b4354333550414a4151445057334b333337454b36534a5a444d5145564c58414836354b375a565a4d4c58594e222c20226b6579223a20223034356532613735383962373363313964353334316366313261633063356636633435633239386434633230303032633739346461616461666462383366333566356265323339363336343864376161636366356532373338303366326665633761386630656234643438343563396238396139373262346130393239386231376522207d0a20205d2c0a20202272756c6573223a205b0a202020207b20226e616d65223a202261646d696e222c202273636f7065223a207b202274797065223a202273656c662d61646d696e22207d2c0a20202020202020227072696e636970616c73223a207b202274797065223a2022616c6c222c20227369676e657273223a205b2261646d696e225d207d207d0a20205d2c0a2020227265636f76657279223a207b0a202020202270726f66696c65223a20226c6f7373222c0a20202020226d6f6465223a207b202274797065223a20227a6b2d6f6e6c79222c20227665726966696572223a20224341414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414149544134222c2022636972637569742d6964223a2022616222207d2c0a2020202022636f6e74726f6c6c6572223a202243414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141484b334d222c0a20202020227265706c61636561626c65223a205b2261646d696e225d2c0a202020202264656c61792d6c656467657273223a20352c0a20202020226578706972792d6c656467657273223a20313030302c0a20202020226d61782d63616e63656c73223a20332c0a202020202270656e64696e672d6163746976697479223a2022636f6e74696e7565220a20207d0a7d" + }, + { + "map": [ + { + "key": { + "symbol": "guardians" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_nullifier" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "zk_proof" + }, + "val": { + "vec": [] + } + } + ] + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "sequence_number": 0, + "timestamp": 0, + "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "admin" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [ + { + "u32": 1 + } + ] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "ContextRuleData" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "context_type" + }, + "val": { + "vec": [ + { + "symbol": "CallContract" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "recovery" + } + }, + { + "key": { + "symbol": "policy_ids" + }, + "val": { + "vec": [ + { + "u32": 0 + } + ] + } + }, + { + "key": { + "symbol": "signer_ids" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": "void" + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyData" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "policy" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PolicyLookup" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerData" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "signer" + }, + "val": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN" + }, + { + "bytes": "045e2a7589b73c19d5341cf12ac0c5f6c45c298d4c20002c794daadafdb83f35f5be23963648d7aaccf5e273803f2fec7a8f0eb4d4845c9b89a972b4a09298b17e" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "SignerLookup" + }, + { + "bytes": "1d54bdd76de3c8520bd7be2a1e3f00574943b2d05d83bd194d7088f284e72676" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "symbol": "AppliedDoc" + }, + "val": { + "bytes": "dba4dd953a5a3a74934b5b64aeadfa34c19c5e3c0b41911e936aa9cae0edc978" + } + }, + { + "key": { + "symbol": "RecoveryController" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Count" + } + ] + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPolicyId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextSignerId" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Attempt" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "action" + }, + "val": { + "vec": [ + { + "symbol": "LostKey" + } + ] + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "evidence_deadline" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "executable_after" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expires_at" + }, + "val": { + "u32": 1005 + } + }, + { + "key": { + "symbol": "guardian_approvals" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "nullifier" + }, + "val": { + "vec": [ + { + "bytes": "0909090909090909090909090909090909090909090909090909090909090909" + } + ] + } + }, + { + "key": { + "symbol": "replaced_credentials" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "vec": [ + { + "symbol": "AuthorizedPending" + } + ] + } + }, + { + "key": { + "symbol": "target_doc_hash" + }, + "val": { + "bytes": "469eff78a84a6666bf266df3f2a4150538dfb52df555a554b05416df84cb2609" + } + }, + { + "key": { + "symbol": "zk_verified" + }, + "val": { + "bool": true + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Config" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "baseline" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "controller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "delay_ledgers" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "expiry_ledgers" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "max_cancels" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "mode" + }, + "val": { + "vec": [ + { + "symbol": "ZkOnly" + }, + { + "map": [ + { + "key": { + "symbol": "circuit_id" + }, + "val": { + "bytes": "ab" + } + }, + { + "key": { + "symbol": "pool" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "verifier" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "pending_activity" + }, + "val": { + "vec": [ + { + "symbol": "Continue" + } + ] + } + }, + { + "key": { + "symbol": "profile" + }, + "val": { + "vec": [ + { + "symbol": "Loss" + } + ] + } + }, + { + "key": { + "symbol": "replaceable" + }, + "val": { + "vec": [ + { + "bytes": "1cdd641e4082e526053899cb26dec64a2856f0243c42fdccafa116c9d437d440" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "NextAttemptId" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1" + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "vec": [ + { + "symbol": "Nullifier" + }, + { + "bytes": "0909090909090909090909090909090909090909090909090909090909090909" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBYWKTO6IALDRI7LQM2IBHK7SDKXKO5JTMJCVQVKEI4XMJ724ZVJI2YM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCUU7RYG23ZBZZCKS2PPSZ2GJIBTBYXF47GZCYG5PUBN54Z7AKQBF2SY", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CCYWLNWRYDCAEM2A2EMTWAMIGWESQGUJNDTRRFIOS5CBPRO54EZ27ABG", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CDC6GSNZCBZGAP3GMM4MNHEGOQKDJBZ5FHAWWHYO7NV6NXMKMGEGVEJV", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CD4IF75DNQJKCT35PAJAQDPW3K337EK6SJZDMQEVLXAH65K7ZVZMLXYN", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs index 1d779cc..a5cbb38 100644 --- a/crates/integration-tests/tests/recovery.rs +++ b/crates/integration-tests/tests/recovery.rs @@ -161,6 +161,46 @@ fn combined_mode_json( ) } +/// Same shape as `enroll_doc`, but `protected` profile with a baseline +/// commitment — for `begin_compromise_attempt` tests, which require one. +fn enroll_doc_with_baseline( + controller: &Address, + guardians: &[Address], + quorum: u32, + baseline_doc_hash_hex: &str, +) -> std::string::String { + let guardian_list = guardians + .iter() + .map(|g| format!("\"{}\"", strkey(g))) + .collect::>() + .join(","); + format!( + r#"{{ + "version": 1, + "network": "{FIXTURE_NETWORK}", + "signers": [ + {{ "id": "admin", "verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}" }} + ], + "rules": [ + {{ "name": "admin", "scope": {{ "type": "self-admin" }}, + "principals": {{ "type": "all", "signers": ["admin"] }} }} + ], + "recovery": {{ + "profile": "protected", + "mode": {{ "type": "guardian-only", "guardians": [{guardian_list}], "quorum": {quorum} }}, + "controller": "{controller}", + "baseline": {{ "doc-hash": "{baseline_doc_hash_hex}" }}, + "replaceable": ["admin"], + "delay-ledgers": 5, + "expiry-ledgers": 1000, + "max-cancels": 3, + "pending-activity": "continue" + }} +}}"#, + controller = strkey(controller), + ) +} + fn recovery_rule_id(w: &World) -> u32 { let client = w.account_client(); let n = client.get_context_rules_count(); @@ -656,3 +696,148 @@ fn zk_cancellation_is_refused_once_the_attempt_has_completed() { perch_recovery::types::AttemptState::Completed ); } + +#[test] +fn begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + + let baseline_hash = BytesN::from_array(&w.env, &[3u8; 32]); + let baseline_hex = hex::encode(baseline_hash.to_array()); + let doc = enroll_doc_with_baseline(&controller, std::slice::from_ref(&g1), 1, &baseline_hex); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + + // A caller-chosen target that doesn't match the enrolled baseline must be + // refused — otherwise `baseline` would be decorative and any document + // could be authorized as "compromise recovery". + let not_the_baseline = BytesN::from_array(&w.env, &[4u8; 32]); + assert!(recovery + .try_begin_compromise_attempt(&w.account, ¬_the_baseline, &replaceable) + .is_err()); + assert!(!recovery.has_pending(&w.account)); + + // The actual baseline is accepted. + assert!(recovery + .try_begin_compromise_attempt(&w.account, &baseline_hash, &replaceable) + .is_ok()); + assert!(recovery.has_pending(&w.account)); +} + +#[test] +fn a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + let g2 = Address::generate(&w.env); + + // 2-of-2 quorum, so a lone declared attempt never gets any evidence. + let doc = enroll_doc(&controller, &[g1.clone(), g2.clone()], 2); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target = target_doc(&controller, &[g1.clone(), g2.clone()], 2); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + assert!(recovery.has_pending(&w.account)); + + // Before the fix, a `CollectingEvidence` attempt was live forever — + // permissionlessly declared, with no guardian or ZK evidence ever + // required to arrive, it would block every ordinary `apply_doc` call + // indefinitely via `guard_apply_doc`'s unconditional pending-attempt + // check. `evidence_deadline` (enrolled `expiry-ledgers` after + // `begin_lost_key_attempt`) bounds that. + let attempt = recovery.get_attempt(&w.account).unwrap(); + w.env + .ledger() + .with_mut(|l| l.sequence_number = attempt.evidence_deadline); + assert!(!recovery.has_pending(&w.account)); + + // An ordinary admin `apply_doc` (re-applying the same document) is no + // longer blocked. + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + + // And a fresh attempt can be declared, replacing the lapsed one. + assert!(recovery + .try_begin_lost_key_attempt(&w.account, &target_hash, &replaceable) + .is_ok()); +} + +#[test] +fn zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let verifier = w.env.register(MockZkVerifier, ()); + + let doc = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let target = enroll_doc_with_mode(&controller, &zk_only_mode_json(&verifier)).replace( + &format!(r#""verifier": "{ADMIN_VERIFIER}", "key": "{ADMIN_KEY}""#), + &format!(r#""verifier": "{NEW_ADMIN_VERIFIER}", "key": "{NEW_ADMIN_KEY}""#), + ); + let target_bytes = Bytes::from_slice(&w.env, target.as_bytes()); + let target_hash: BytesN<32> = w.env.crypto().sha256(&target_bytes).to_bytes(); + let replaceable = recovery.get_config(&w.account).unwrap().replaceable; + recovery.begin_lost_key_attempt(&w.account, &target_hash, &replaceable); + + let shared_nullifier = BytesN::from_array(&w.env, &[9u8; 32]); + let proof = Bytes::from_array(&w.env, &[1u8; 1]); + recovery.submit_zk_proof(&w.account, &shared_nullifier, &proof); + assert!(recovery.get_attempt(&w.account).unwrap().zk_verified); + + // The same nullifier, reused for the *cancellation* domain on this same + // attempt, must be refused immediately — it's already spent. Before the + // fix, `submit_zk_proof` deferred reservation to `complete`, so at this + // point (attempt not yet completed) this call would have wrongly + // succeeded, letting one nullifier authorize two different statements. + assert!(recovery + .try_submit_zk_cancel(&w.account, &shared_nullifier, &proof) + .is_err()); +} + +#[test] +fn credential_fingerprint_is_hex_case_insensitive() { + let w = setup(); + let controller = w.env.register(PerchRecovery, ()); + let recovery = PerchRecoveryClient::new(&w.env, &controller); + let g1 = Address::generate(&w.env); + + let doc = enroll_doc(&controller, std::slice::from_ref(&g1), 1); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let fingerprint_lower = recovery.get_config(&w.account).unwrap().replaceable; + + // Re-apply the identical document, except the admin signer's key is + // spelled in upper case — the same physical credential, different hex + // casing. A revocation fingerprint keyed on the raw text instead of the + // decoded bytes would treat this as a different credential, letting a + // later document reintroduce a revoked physical key just by respelling + // it. + let doc_upper = doc.replace(ADMIN_KEY, &ADMIN_KEY.to_uppercase()); + assert_ne!(doc, doc_upper, "test fixture must actually vary the casing"); + w.account_client().apply_doc( + &Bytes::from_slice(&w.env, doc_upper.as_bytes()), + &no_recovery_evidence(&w.env), + ); + let fingerprint_upper = recovery.get_config(&w.account).unwrap().replaceable; + + assert_eq!(fingerprint_lower, fingerprint_upper); +} diff --git a/crates/perch-doc-compiler/Cargo.toml b/crates/perch-doc-compiler/Cargo.toml index d83b12e..11b01bf 100644 --- a/crates/perch-doc-compiler/Cargo.toml +++ b/crates/perch-doc-compiler/Cargo.toml @@ -28,10 +28,15 @@ perch-program = { workspace = true } # honored — cargo ignores it on workspace-inherited deps. Keeps both no_std. perch-ir = { version = "0.1.1", path = "../perch-ir", default-features = false, optional = true } perch-compile = { version = "0.1.1", path = "../perch-compile", default-features = false, optional = true } +# Decodes a signer's hex-encoded key to its physical bytes before fingerprinting +# it (see `credential_fingerprint`) — matching `perch-ir::validate`'s own +# hex-decode-before-compare treatment so two casings of the same key fingerprint +# identically. +hex = { version = "0.4", default-features = false, features = ["alloc"], optional = true } [features] # The deployable contract + its parser/compiler deps. Consumers that only # need the wire types and DocCompilerClient (i.e. smart accounts) disable # this so their wasm links no compiler code. default = ["contract"] -contract = ["dep:perch-ir", "dep:perch-compile"] +contract = ["dep:perch-ir", "dep:perch-compile", "dep:hex"] diff --git a/crates/perch-doc-compiler/src/lib.rs b/crates/perch-doc-compiler/src/lib.rs index 1f4f220..23a6414 100644 --- a/crates/perch-doc-compiler/src/lib.rs +++ b/crates/perch-doc-compiler/src/lib.rs @@ -318,28 +318,36 @@ fn to_compiled_recovery( } /// `sha256` of a tagged encoding of a signer's physical credential — the -/// verifier+key pair for `external`, the address for `delegated`. Used only -/// to fingerprint a `replaceable` signer's credential identity, never the -/// document-local id string, so revocation survives that id being reused for -/// a different physical key in a later document. +/// verifier+decoded-key bytes for `external`, the address for `delegated`. +/// Used only to fingerprint a `replaceable` signer's credential identity, +/// never the document-local id string, so revocation survives that id being +/// reused for a different physical key in a later document. +/// +/// `key` is hex-decoded to its physical bytes before hashing — `perch-ir` +/// validation already treats hex casing as insignificant for the same +/// physical key (`crates/perch-ir/src/validate.rs`'s `seen_key_material` +/// keys on decoded bytes, not the spelling), so fingerprinting the raw text +/// instead would let the same credential, re-declared with different hex +/// casing, evade a prior revocation entirely. #[cfg(feature = "contract")] fn credential_fingerprint(e: &Env, method: &perch_ir::SignerMethod) -> BytesN<32> { - let mut buf = alloc::string::String::new(); + let mut buf = alloc::vec::Vec::new(); match method { perch_ir::SignerMethod::External { verifier, key } => { - buf.push_str("external|"); - buf.push_str(verifier); - buf.push('|'); - buf.push_str(key); + buf.extend_from_slice(b"external|"); + buf.extend_from_slice(verifier.as_bytes()); + buf.push(b'|'); + // Already validated hex by the time a document reaches the + // compiler (`perch_ir::validate`) — decode failure here would + // mean validation was skipped, not a reachable user input. + buf.extend_from_slice(&hex::decode(key).unwrap_or_default()); } perch_ir::SignerMethod::Delegated { address } => { - buf.push_str("delegated|"); - buf.push_str(address); + buf.extend_from_slice(b"delegated|"); + buf.extend_from_slice(address.as_bytes()); } } - e.crypto() - .sha256(&Bytes::from_slice(e, buf.as_bytes())) - .to_bytes() + e.crypto().sha256(&Bytes::from_slice(e, &buf)).to_bytes() } #[cfg(feature = "contract")] diff --git a/crates/perch-recovery/Cargo.toml b/crates/perch-recovery/Cargo.toml index e9171b3..2fae6d1 100644 --- a/crates/perch-recovery/Cargo.toml +++ b/crates/perch-recovery/Cargo.toml @@ -18,7 +18,10 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -soroban-sdk-tools = { workspace = true, optional = true } +# Required unconditionally: `src/lib.rs`'s always-available `RecoveryError` +# uses `#[scerr]` regardless of the `contract` feature, so a client-only build +# (perch-smart-account, `default-features = false`) still needs this. +soroban-sdk-tools = { workspace = true } stellar-accounts = { workspace = true, optional = true } perch-doc-compiler = { version = "0.2.1", path = "../perch-doc-compiler", default-features = false } @@ -33,4 +36,4 @@ perch-ir = { path = "../perch-ir" } # perch-smart-account) disable this so their wasm links no controller state # or lifecycle code — mirrors perch-doc-compiler's `contract` feature split. default = ["contract"] -contract = ["dep:soroban-sdk-tools", "dep:stellar-accounts"] +contract = ["dep:stellar-accounts"] diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs index d51988b..91aa9ac 100644 --- a/crates/perch-recovery/src/contract.rs +++ b/crates/perch-recovery/src/contract.rs @@ -60,7 +60,19 @@ pub struct RecoveryAuthorized { } const TTL_THRESHOLD: u32 = 1; -const TTL_EXTEND: u32 = 3_110_400; // ~180 days at 5s ledgers; an operational tuning knob, not a security parameter. + +/// The network's current maximum persistent-entry TTL — the longest any +/// extension in this module can buy, recomputed at each call site rather +/// than pinned to a constant so it tracks the live network configuration +/// (see `Env::storage().max_ttl()`'s own doc comment). This is deliberately +/// *not* "forever": a persistent entry with no further activity still +/// expires once this many ledgers pass with no renewal. [`PerchRecovery::renew`] +/// is the explicit, permissionless keep-alive for exactly that gap — see +/// `docs/recovery/controller-governance.md`'s "Keeping permanent state +/// alive" section. +fn max_ttl(e: &Env) -> u32 { + e.storage().max_ttl() +} #[contract] pub struct PerchRecovery; @@ -69,18 +81,28 @@ pub struct PerchRecovery; impl Policy for PerchRecovery { type AccountParams = CompiledRecoveryConfig; - /// Write the enrolled configuration. The security gate already ran (see - /// module docs) — this only shape-checks the rule it's attached to and - /// stores the value. + /// Write the enrolled configuration. `install`/`enforce`/`uninstall` are + /// exported functions on this contract like any other — reachable by a + /// direct call from anyone, not only via OZ's real install flow — so this + /// first line is load-bearing, not defensive boilerplate: it makes a + /// direct, forged call fail before touching storage. It succeeds for free + /// on the real path (`smart_account`'s own wasm is the direct invoker + /// when `apply_doc` re-installs its context rules — Soroban's + /// invoker-contract authorization, `require_auth`'s first-checked path, + /// grants this without a signature) and fails for any caller that isn't + /// `smart_account` itself. The reconfiguration gate already ran upstream + /// (see module docs) — this only shape-checks the rule it's attached to + /// and stores the value. fn install( e: &Env, install_params: CompiledRecoveryConfig, context_rule: ContextRule, smart_account: Address, ) { + smart_account.require_auth(); assert_self_zero_signer_rule(e, &context_rule, &smart_account); RecoveryStorage::set_config(e, &smart_account, &install_params); - RecoveryStorage::extend_config_ttl(e, &smart_account, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_config_ttl(e, &smart_account, TTL_THRESHOLD, max_ttl(e)); } /// Variant A completion: authorize `apply_doc` exactly when a live, @@ -114,6 +136,18 @@ fn assert_self_zero_signer_rule(e: &Env, rule: &ContextRule, smart_account: &Add } fn complete(e: &Env, context: &Context, smart_account: &Address) { + // Same reasoning as `install`: `enforce` is a directly-callable exported + // function, and `context` is an ordinary argument the caller fully + // controls — nothing about receiving a `Context::Contract` value proves + // it reflects a real invocation. Without this, anyone who knows (or + // reconstructs) the pending attempt's target document bytes could call + // `enforce` directly with a forged `context`, consuming the attempt + // (revoking credentials, spending its nullifier) without `apply_doc`'s + // body ever having run. This succeeds for free on the real path — OZ's + // `do_check_auth`, itself running because `apply_doc` required + // `smart_account`'s own auth, is the direct invoker of this cross-call — + // and fails for a direct, unrelated caller. + smart_account.require_auth(); let Context::Contract(ContractContext { contract, fn_name, @@ -162,10 +196,10 @@ fn complete(e: &Env, context: &Context, smart_account: &Address) { } } RecoveryStorage::set_revoked(e, smart_account, &revoked); - RecoveryStorage::extend_revoked_ttl(e, smart_account, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_revoked_ttl(e, smart_account, TTL_THRESHOLD, max_ttl(e)); if let Some(n) = attempt.nullifier.first() { RecoveryStorage::set_nullifier(e, &n, &true); - RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, max_ttl(e)); } RecoveryCompleted { account: smart_account.clone(), @@ -183,26 +217,40 @@ impl PerchRecovery { /// `account`. See module docs for why this — not `install`/`uninstall` — /// is the actual security gate. /// - /// Blocks unconditionally while a live attempt exists (property 9, - /// independent of §7 — see `docs/recovery/section-7-gate.md`). This also - /// reads `false` for a legitimate completion call, because `enforce` - /// already consumed the attempt during auth evaluation, before this runs. + /// Blocks unconditionally while a live attempt exists — this holds + /// regardless of the account's chosen pending-activity policy (see + /// `docs/recovery/pending-activity-policy.md`, a separate, still-open + /// question about *ordinary* activity during a pending attempt). This + /// also reads `false` for a legitimate completion call, because + /// `enforce` already consumed the attempt during auth evaluation, before + /// this runs. /// /// Otherwise: no change is always fine; a first enrollment (`old` is - /// `None`) is always fine; a change while `old.profile == Loss` is always - /// fine (§2.1); a change while `old.profile == Protected` — including - /// removing recovery — requires `evidence` to satisfy the *currently* - /// enrolled condition over a digest binding this exact transition. This - /// generalizes the validated experiment's strictly-additive-only - /// reconfigure (which remains reachable as the common case) to the full - /// requirement in the authoritative decision record §2 — see - /// `docs/recovery/controller-governance.md`. + /// `None`) is always fine; a change while `old.profile == Loss` is + /// always fine (ordinary admin authorization, already established by + /// `apply_doc`'s own `require_auth`, is enough); a change while + /// `old.profile == Protected` — including removing recovery — requires + /// `evidence` to satisfy the *currently* enrolled condition over a + /// digest binding this exact transition. This generalizes a companion + /// smart-account implementation's validated strictly-additive-only + /// reconfigure (which remains reachable as the common case) to a fully + /// general rule — see `docs/recovery/controller-governance.md`. pub fn guard_apply_doc( e: &Env, account: Address, new_recovery: Vec, evidence: ReconfigureEvidence, ) -> Result<(), RecoveryError> { + // Also directly callable like `install`/`enforce` above — without + // this, anyone who obtains valid reconfigure evidence (e.g. by + // observing the real `apply_doc` transaction before it lands) could + // call this entry point standalone, burning a one-time ZK nullifier + // or a guardian's signature with no document ever changing — + // front-running and denial-of-service against the real + // reconfiguration. Succeeds for free on the real path: + // `perch-smart-account`'s `apply_doc` is the direct invoker of this + // cross-call, before it touches any context rule. + account.require_auth(); if let Some(attempt) = RecoveryStorage::get_attempt(e, &account) { if is_live(e, &attempt) { return Err(RecoveryError::AttemptPending); @@ -210,6 +258,15 @@ impl PerchRecovery { } let old = RecoveryStorage::get_config(e, &account); + // Renew here too (not only via `require_config`, which this function + // deliberately doesn't use since it must distinguish "no config" from + // "config present") — every `apply_doc` call reaches this point, so + // this is the read path that actually needs to keep an enrolled + // `Protected` config from expiring into a false "first enrollment" + // (which would let a reconfiguration skip the evidence requirement). + if old.is_some() { + RecoveryStorage::extend_config_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); + } let new = new_recovery.first(); if config_matches(&old, new.as_ref()) { @@ -276,7 +333,10 @@ impl PerchRecovery { /// Declare intent to restore the enrolled baseline after suspected /// compromise, with `replaced_credentials` replaced. Requires a baseline - /// to be enrolled. See `begin_lost_key_attempt` for the shared mechanics. + /// to be enrolled, and `target_doc_hash` to equal it exactly — a + /// compromise attempt targets *the* approved baseline, never a + /// caller-chosen document (that's what `begin_lost_key_attempt` is for). + /// See `begin_lost_key_attempt` for the shared mechanics. pub fn begin_compromise_attempt( e: &Env, account: Address, @@ -284,8 +344,11 @@ impl PerchRecovery { replaced_credentials: Vec>, ) -> Result { let config = require_config(e, &account)?; - if config.baseline.is_empty() { + let Some(baseline) = config.baseline.first() else { return Err(RecoveryError::NoBaselineEnrolled); + }; + if baseline != target_doc_hash { + return Err(RecoveryError::TargetNotBaseline); } begin_attempt( e, @@ -296,13 +359,17 @@ impl PerchRecovery { ) } - /// A guardian approves this account's pending attempt's initiation. + /// A guardian approves this account's pending attempt's initiation. The + /// guardian's signature is bound to a digest bound to *this exact* + /// attempt (id, action, target, enrolled config) — not just to the fixed + /// `(account, guardian)` argument pair — so a delayed or replayed + /// signature can never be redirected to authorize a different attempt + /// than the guardian actually approved. pub fn submit_guardian_approval( e: &Env, account: Address, guardian: Address, ) -> Result<(), RecoveryError> { - guardian.require_auth(); let config = require_config(e, &account)?; let g = guardian_set(&config.mode).ok_or(RecoveryError::ModeHasNoGuardians)?; if !g.guardians.contains(&guardian) { @@ -312,13 +379,28 @@ impl PerchRecovery { if attempt.state != AttemptState::CollectingEvidence { return Err(RecoveryError::AttemptNotAuthorized); } + if !is_live(e, &attempt) { + return Err(RecoveryError::NoLiveAttempt); + } if attempt.guardian_approvals.contains(&guardian) { return Err(RecoveryError::AlreadyApproved); } + let cfg_hash = config_hash_of(e, &config); + let digest = zk::statement( + e, + &account, + &e.current_contract_address(), + &attempt.action, + &cfg_hash, + Some(&attempt.target_doc_hash), + attempt.id, + config.delay_ledgers, + ); + guardian.require_auth_for_args(Vec::from_array(e, [digest.into_val(e)])); attempt.guardian_approvals.push_back(guardian); - maybe_promote(e, &config, &mut attempt); + maybe_promote(e, &account, &config, &mut attempt)?; RecoveryStorage::set_attempt(e, &account, &attempt); - RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); Ok(()) } @@ -341,6 +423,9 @@ impl PerchRecovery { if attempt.zk_verified { return Ok(()); // idempotent re-submission } + if !is_live(e, &attempt) { + return Err(RecoveryError::NoLiveAttempt); + } if RecoveryStorage::get_nullifier(e, &nullifier).unwrap_or(false) { return Err(RecoveryError::NullifierAlreadySpent); } @@ -358,18 +443,24 @@ impl PerchRecovery { if !ZkVerifierClient::new(e, &z.verifier).verify_proof(&stmt, &nullifier, &proof, &z.pool) { return Err(RecoveryError::ZkProofInvalid); } + // Reserved immediately, not deferred to `complete` — otherwise the + // same nullifier could pass this check again for a second, separate + // account-bound statement before either attempt completes (`complete` + // never re-checks the global set, only writes it). + RecoveryStorage::set_nullifier(e, &nullifier, &true); + RecoveryStorage::extend_nullifier_ttl(e, &nullifier, TTL_THRESHOLD, max_ttl(e)); attempt.zk_verified = true; let mut nul = Vec::new(e); nul.push_back(nullifier); attempt.nullifier = nul; - maybe_promote(e, &config, &mut attempt); + maybe_promote(e, &account, &config, &mut attempt)?; RecoveryStorage::set_attempt(e, &account, &attempt); - RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); Ok(()) } /// A guardian approves cancellation of this account's identified attempt. - /// Own action domain — initiation approvals never count here (§2.2). + /// Own action domain — initiation approvals never count here. /// Only actually cancels once the mode's full cancellation evidence set /// is present: for `Combined`, reaching guardian quorum here is not /// enough by itself — a verified ZK cancellation proof is also required @@ -379,7 +470,6 @@ impl PerchRecovery { account: Address, guardian: Address, ) -> Result<(), RecoveryError> { - guardian.require_auth(); let config = require_config(e, &account)?; let g = guardian_set(&config.mode).ok_or(RecoveryError::ModeHasNoGuardians)?; if !g.guardians.contains(&guardian) { @@ -389,6 +479,23 @@ impl PerchRecovery { if !is_live(e, &attempt) { return Err(RecoveryError::NoLiveAttempt); } + // Bound to this exact attempt and the cancellation domain — the same + // digest shape `submit_zk_cancel` verifies a proof against below — + // not just the fixed `(account, guardian)` arguments, so a delayed + // signature can't be redirected to cancel a different, later attempt + // than the one the guardian actually signed for. + let cfg_hash = config_hash_of(e, &config); + let digest = zk::statement( + e, + &account, + &e.current_contract_address(), + &Action::Cancel, + &cfg_hash, + None, + attempt.id, + config.delay_ledgers, + ); + guardian.require_auth_for_args(Vec::from_array(e, [digest.into_val(e)])); let key = (account.clone(), attempt.id); let mut tally = RecoveryStorage::get_cancel_tally(e, &key).unwrap_or_else(|| Vec::new(e)); if tally.contains(&guardian) { @@ -396,7 +503,7 @@ impl PerchRecovery { } tally.push_back(guardian); RecoveryStorage::set_cancel_tally(e, &key, &tally); - RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_cancel_tally_ttl(e, &key, TTL_THRESHOLD, max_ttl(e)); let guardian_quorum_reached = tally.len() >= g.quorum; if guardian_quorum_reached { let zk_cancel_verified = @@ -444,9 +551,10 @@ impl PerchRecovery { return Err(RecoveryError::ZkProofInvalid); } RecoveryStorage::set_nullifier(e, &nullifier, &true); + RecoveryStorage::extend_nullifier_ttl(e, &nullifier, TTL_THRESHOLD, max_ttl(e)); let key = (account.clone(), attempt.id); RecoveryStorage::set_zk_cancel_verified(e, &key, &true); - RecoveryStorage::extend_zk_cancel_verified_ttl(e, &key, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_zk_cancel_verified_ttl(e, &key, TTL_THRESHOLD, max_ttl(e)); let guardian_quorum_reached = match guardian_set(&config.mode) { Some(g) => { RecoveryStorage::get_cancel_tally(e, &key) @@ -485,23 +593,69 @@ impl PerchRecovery { pub fn has_pending(e: &Env, account: Address) -> bool { RecoveryStorage::get_attempt(e, &account).is_some_and(|a| is_live(e, &a)) } + + /// Extend every one of `account`'s existing recovery entries — enrolled + /// config, current/most recent attempt (and its nullifier, if spent), + /// permanent revoked set, and the lifetime counters — to the network's + /// current maximum TTL. Permissionless and idempotent: it only ever + /// extends state that's already there, never changes what it means, so + /// anyone (a keeper script, a wallet's own background job) can call this + /// periodically for an account with no other recovery activity. Soroban + /// persistent entries have a finite maximum TTL — nothing renews them on + /// its own absent an explicit touch like this one, or the account + /// otherwise using recovery (`require_config`'s own read-path renewal + /// covers the busier entries already). See + /// `docs/recovery/controller-governance.md`'s "Keeping permanent state + /// alive" section. + pub fn renew(e: &Env, account: Address) { + let ttl = max_ttl(e); + if RecoveryStorage::has_config(e, &account) { + RecoveryStorage::extend_config_ttl(e, &account, TTL_THRESHOLD, ttl); + } + if let Some(attempt) = RecoveryStorage::get_attempt(e, &account) { + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, ttl); + if let Some(n) = attempt.nullifier.first() { + RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, ttl); + } + } + if RecoveryStorage::has_revoked(e, &account) { + RecoveryStorage::extend_revoked_ttl(e, &account, TTL_THRESHOLD, ttl); + } + if RecoveryStorage::has_cancels_used(e, &account) { + RecoveryStorage::extend_cancels_used_ttl(e, &account, TTL_THRESHOLD, ttl); + } + if RecoveryStorage::has_next_attempt_id(e, &account) { + RecoveryStorage::extend_next_attempt_id_ttl(e, &account, TTL_THRESHOLD, ttl); + } + } } fn require_config(e: &Env, account: &Address) -> Result { - RecoveryStorage::get_config(e, account).ok_or(RecoveryError::NotEnrolled) + let config = RecoveryStorage::get_config(e, account).ok_or(RecoveryError::NotEnrolled)?; + // Renews on every real use (every begin/approve/proof/cancel call), not + // just on install — an enrolled config that nobody ever touches for + // longer than the network's max TTL would otherwise silently expire, + // and `guard_apply_doc` treats a missing config as "first enrollment" + // (no evidence required), which would let a `Protected` account's + // reconfiguration gate quietly fail open. See [`PerchRecovery::renew`] + // for the explicit keep-alive covering accounts with no such activity. + RecoveryStorage::extend_config_ttl(e, account, TTL_THRESHOLD, max_ttl(e)); + Ok(config) } fn require_attempt(e: &Env, account: &Address) -> Result { RecoveryStorage::get_attempt(e, account).ok_or(RecoveryError::NoLiveAttempt) } -/// Live = not terminal, and (if authorized) not past its expiry. A -/// `CollectingEvidence` attempt has no expiry of its own in this design (only -/// an authorized attempt's *completion window* expires) — it is live until -/// explicitly cancelled or replaced by a fresh `begin_*_attempt` call. +/// Live = not terminal, and not past its current phase's deadline. A +/// `CollectingEvidence` attempt is live until `evidence_deadline` — without +/// this bound, a single permissionless `begin_*_attempt` call would block +/// every ordinary `apply_doc` (via `guard_apply_doc`'s unconditional +/// live-attempt check) indefinitely, since nothing else forces the mode's +/// evidence to ever actually arrive. fn is_live(e: &Env, attempt: &Attempt) -> bool { match attempt.state { - AttemptState::CollectingEvidence => true, + AttemptState::CollectingEvidence => e.ledger().sequence() < attempt.evidence_deadline, AttemptState::AuthorizedPending => e.ledger().sequence() < attempt.expires_at, AttemptState::Completed | AttemptState::Cancelled => false, } @@ -546,7 +700,7 @@ fn initiation_satisfied(mode: &CompiledRecoveryMode, attempt: &Attempt) -> bool } } -/// Mirrors `initiation_satisfied` for the cancellation domain (§2.2): +/// Mirrors `initiation_satisfied` for the cancellation domain: /// `GuardianOnly`/`ZkOnly` need only their own factor, `Combined` needs both /// a guardian quorum AND a valid ZK cancellation proof for the same attempt — /// neither factor alone may cancel a `Combined`-mode attempt. @@ -562,22 +716,33 @@ fn cancellation_satisfied( } } -fn maybe_promote(e: &Env, config: &CompiledRecoveryConfig, attempt: &mut Attempt) { +fn maybe_promote( + e: &Env, + account: &Address, + config: &CompiledRecoveryConfig, + attempt: &mut Attempt, +) -> Result<(), RecoveryError> { if attempt.state == AttemptState::CollectingEvidence && initiation_satisfied(&config.mode, attempt) { let now = e.ledger().sequence(); attempt.state = AttemptState::AuthorizedPending; - attempt.executable_after = now + config.delay_ledgers; - attempt.expires_at = attempt.executable_after + config.expiry_ledgers; + attempt.executable_after = now + .checked_add(config.delay_ledgers) + .ok_or(RecoveryError::TimelockOverflow)?; + attempt.expires_at = attempt + .executable_after + .checked_add(config.expiry_ledgers) + .ok_or(RecoveryError::TimelockOverflow)?; RecoveryAuthorized { - account: e.current_contract_address(), + account: account.clone(), attempt_id: attempt.id, executable_after: attempt.executable_after, expires_at: attempt.expires_at, } .publish(e); } + Ok(()) } fn begin_attempt( @@ -611,13 +776,27 @@ fn begin_attempt( } let id = RecoveryStorage::get_next_attempt_id(e, &account).unwrap_or(0); RecoveryStorage::set_next_attempt_id(e, &account, &(id + 1)); + // The nonce must never be reused (a replayed id would let old per-attempt + // evidence/statements collide with a new attempt) — extend on every + // write, not just at install, so this counter can't silently reset to 0 + // via TTL expiry during long account inactivity. + RecoveryStorage::extend_next_attempt_id_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); + let created_at = e.ledger().sequence(); let attempt = Attempt { id, action, target_doc_hash, replaced_credentials, - created_at: e.ledger().sequence(), + created_at, + // Bounds how long a permissionless `begin_*_attempt` call can hold + // `guard_apply_doc`'s unconditional live-attempt block open while no + // evidence arrives — see `is_live`. Reuses `expiry_ledgers` (already + // the account's own configured "how long this recovery gets" budget) + // rather than adding a new schema field for the same kind of window. + evidence_deadline: created_at + .checked_add(config.expiry_ledgers) + .ok_or(RecoveryError::TimelockOverflow)?, executable_after: 0, expires_at: 0, guardian_approvals: Vec::new(e), @@ -626,7 +805,7 @@ fn begin_attempt( state: AttemptState::CollectingEvidence, }; RecoveryStorage::set_attempt(e, &account, &attempt); - RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, TTL_EXTEND); + RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); Ok(id) } @@ -637,6 +816,9 @@ fn cancel_attempt(e: &Env, account: &Address, attempt: &mut Attempt) -> Result<( return Err(RecoveryError::MaxCancelsReached); } RecoveryStorage::set_cancels_used(e, account, &(used + 1)); + // A lifetime griefing-cancellation cap only bounds anything if it can't + // silently reset to 0 via TTL expiry — extend on every write. + RecoveryStorage::extend_cancels_used_ttl(e, account, TTL_THRESHOLD, max_ttl(e)); attempt.state = AttemptState::Cancelled; if let Some(n) = attempt.nullifier.first() { RecoveryStorage::set_nullifier(e, &n, &false); @@ -690,5 +872,6 @@ fn require_zk_evidence( return Err(RecoveryError::ZkProofInvalid); } RecoveryStorage::set_nullifier(e, &nullifier, &true); + RecoveryStorage::extend_nullifier_ttl(e, &nullifier, TTL_THRESHOLD, max_ttl(e)); Ok(()) } diff --git a/crates/perch-recovery/src/lib.rs b/crates/perch-recovery/src/lib.rs index 35c4b72..df006bd 100644 --- a/crates/perch-recovery/src/lib.rs +++ b/crates/perch-recovery/src/lib.rs @@ -1,9 +1,12 @@ -//! `perch-recovery`: the shared account-recovery controller, generalizing the -//! validated Nido Stage 3 experiment into a perch crate. Guardian-only, -//! ZK-only, and combined recovery modes; strictly-additive-and-beyond +//! `perch-recovery`: the shared account-recovery controller for opt-in +//! account recovery, generalizing a companion smart-account implementation's +//! validated experiment ([nidohq/nido#206]) into a perch crate. Guardian-only, +//! ZK-only, and combined recovery modes; general (not just additive) //! reconfigure under `Protected`; `config_hash` commitment; constructorless -//! deployment. See `docs/recovery/` for the full design, governance, and -//! open release-blocking gate (§7, pending-activity policy). +//! deployment. See `docs/recovery/` for the full design, governance, and the +//! open release-blocking pending-activity-policy gate. +//! +//! [nidohq/nido#206]: https://github.com/nidohq/nido/pull/206 //! //! Mirrors `perch-doc-compiler`'s split: the error type, the evidence type, //! and a client-only cross-contract interface are always available; the @@ -61,9 +64,15 @@ pub enum RecoveryError { ReconfigureEvidenceRequired, /// Suspected-compromise recovery was attempted with no baseline enrolled. NoBaselineEnrolled, + /// A suspected-compromise attempt's target does not match the enrolled + /// baseline's committed hash. + TargetNotBaseline, /// A context rule attached this policy with the wrong shape (non-empty /// signers, or not scoped to the smart account calling itself). MalformedContextRule, + /// Computing this attempt's timelock/expiry ledger sequence would + /// overflow `u32`. + TimelockOverflow, } /// Evidence accompanying an `apply_doc` call that changes a `Protected` diff --git a/crates/perch-recovery/src/storage.rs b/crates/perch-recovery/src/storage.rs index 83d4b7c..9d83c8c 100644 --- a/crates/perch-recovery/src/storage.rs +++ b/crates/perch-recovery/src/storage.rs @@ -21,16 +21,17 @@ pub struct RecoveryStorage { /// Monotonic per-account attempt-id counter — the proposal commitment's /// nonce. Never reused, even across terminal attempts. pub next_attempt_id: PersistentMap, - /// Cumulative cancellations across the account's whole history (§2.2's - /// griefing bound), never reset by a new attempt. + /// Cumulative cancellations across the account's whole history (a + /// griefing bound — see `RecoveryError::MaxCancelsReached`), never reset by + /// a new attempt. pub cancels_used: PersistentMap, /// Guardian cancel-evidence for one `(account, attempt_id)` — a - /// deliberately separate domain from `Attempt::guardian_approvals` - /// (initiation evidence never counts toward cancellation, per §2.2). + /// deliberately separate domain from `Attempt::guardian_approvals`: + /// initiation evidence never counts toward cancellation, and vice versa. pub cancel_tally: PersistentMap<(Address, u64), Vec
>, /// Whether a valid ZK cancellation proof has been verified for one /// `(account, attempt_id)`. Tracked separately from `cancel_tally` so - /// `Combined` mode can require both factors before cancelling (§2.2) — + /// `Combined` mode can require both factors before cancelling — /// each factor's own evidence is recorded independently and /// `cancel_attempt` only fires once the mode's full set is present. pub zk_cancel_verified: PersistentMap<(Address, u64), bool>, diff --git a/crates/perch-recovery/src/types.rs b/crates/perch-recovery/src/types.rs index c2f7cb1..c562c28 100644 --- a/crates/perch-recovery/src/types.rs +++ b/crates/perch-recovery/src/types.rs @@ -7,9 +7,8 @@ use soroban_sdk::{contracttype, Address, BytesN, Vec}; /// Which action a proposal or evidence submission is for. Domain-separates -/// initiation from cancellation from reconfiguration (§2.2): evidence -/// collected for one action never satisfies another, even for the same -/// attempt or account. +/// initiation from cancellation from reconfiguration: evidence collected for +/// one action never satisfies another, even for the same attempt or account. #[derive(Clone, Debug, PartialEq, Eq)] #[contracttype] pub enum Action { @@ -52,10 +51,19 @@ pub struct Attempt { /// Monotonic per-account id — the proposal commitment's nonce. pub id: u64, pub action: Action, - /// `sha256(canonical bytes)` of the exact document this attempt, once - /// completed, installs. Frozen at `begin_attempt` and never recomputed — - /// completion checks the caller's `apply_doc` argument against exactly - /// this value. + /// `sha256` of the exact bytes the caller must submit as `apply_doc`'s + /// `doc_json` argument to complete this attempt. Frozen at + /// `begin_attempt` and never recomputed — completion hashes the raw + /// argument bytes it receives (it runs *before* `apply_doc`'s body, as + /// part of authorization, so it has no access to the compiler's + /// canonical `doc_hash`, which is only computed afterward). Whoever + /// declares this value and whoever later submits the completion call + /// must agree on the exact byte serialization — in practice, both sides + /// should use the canonical bytes (`perch_ir::canonical_json` / + /// `perch-js`'s `canonicalJson`) for consistency with `doc_hash` + /// elsewhere in the system, but nothing here enforces that; a + /// byte-for-byte match against whatever was declared is all that's + /// checked. pub target_doc_hash: BytesN<32>, /// Credential fingerprints this attempt replaces (a subset of the /// enrolled config's `replaceable`, chosen by the caller at initiation — @@ -64,6 +72,13 @@ pub struct Attempt { pub replaced_credentials: Vec>, /// Ledger sequence `begin_attempt` ran at. pub created_at: u32, + /// Ledger sequence at or after which a still-`CollectingEvidence` attempt + /// is no longer live (see [`crate::contract::is_live`]) — bounds how long + /// a permissionless `begin_*_attempt` call can hold `guard_apply_doc`'s + /// unconditional live-attempt block open while no evidence ever arrives. + /// Fixed at `begin_attempt` to `created_at + expiry_ledgers`; irrelevant + /// once the attempt leaves `CollectingEvidence`. + pub evidence_deadline: u32, /// Ledger sequence at or after which this attempt becomes completable, /// once authorized. `0` (unset) while `CollectingEvidence`. pub executable_after: u32, diff --git a/crates/perch-recovery/src/zk.rs b/crates/perch-recovery/src/zk.rs index 2de85aa..c2f1a37 100644 --- a/crates/perch-recovery/src/zk.rs +++ b/crates/perch-recovery/src/zk.rs @@ -1,13 +1,13 @@ //! Generic ZK verifier adapter: the statement a proof must bind, and the //! cross-contract interface any verifier implementation satisfies. //! -//! This module deliberately carries **no circuit**. Per the authoritative -//! decision record §5.4, proof-system portability is an interface goal, not -//! a requirement to ship or validate a specific circuit in this stage — see -//! `docs/recovery/controller-governance.md`'s "ZK adapter scope" section. -//! What this module fixes is the *statement* a proof must be over: every -//! recovery-specific public fact, bound the same way regardless of which -//! proof system a given verifier implements. +//! This module deliberately carries **no circuit** — proof-system +//! portability is an interface goal here, not a requirement to ship or +//! validate a specific circuit; see `docs/recovery/controller-governance.md`'s +//! "ZK adapter scope" section for the full rationale. What this module fixes +//! is the *statement* a proof must be over: every recovery-specific public +//! fact, bound the same way regardless of which proof system a given +//! verifier implements. use crate::types::Action; use soroban_sdk::xdr::ToXdr; @@ -32,11 +32,13 @@ fn action_tag(action: &Action) -> u8 { /// configuration's own content hash (so a proof cannot outlive a /// reconfiguration — see `contract.rs`'s reconfigure-evidence handling); the /// target document's hash for `LostKey`/`Compromise` (zero for `Cancel`, -/// which identifies its attempt by id instead, per §2.2); the attempt's -/// nonce; and the enrolled timelock. Binding all of this in one hash means a -/// proof for one account/action/config/target/attempt can never be replayed -/// for another — every field the follow-up review §5.3 names for a proposal -/// commitment is present. +/// which identifies its attempt by id instead — cancellation and initiation +/// are deliberately separate evidence domains, see `Action`'s own docs); the +/// attempt's nonce; and the enrolled timelock. Binding all of this in one +/// hash means a proof — or, via `submit_guardian_approval`/ +/// `submit_guardian_cancel`, a guardian's signature over the identical +/// digest — for one account/action/config/target/attempt can never be +/// replayed for another. #[allow(clippy::too_many_arguments)] pub fn statement( e: &Env, @@ -87,3 +89,124 @@ pub trait ZkVerifierInterface { pool: Vec
, ) -> bool; } + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::Address as _; + + /// `submit_guardian_approval`/`submit_guardian_cancel` bind a guardian's + /// signature to this statement instead of the bare `(account, guardian)` + /// call arguments, specifically so it can't be replayed against a + /// different attempt, action, or target. That property only holds if the + /// statement actually varies with each of those fields — this pins that + /// down directly, independent of any authorization mocking. + #[test] + fn statement_varies_with_every_distinguishing_field() { + let e = Env::default(); + let account = Address::generate(&e); + let other_account = Address::generate(&e); + let controller = Address::generate(&e); + let other_controller = Address::generate(&e); + let cfg_hash = BytesN::from_array(&e, &[1u8; 32]); + let other_cfg_hash = BytesN::from_array(&e, &[2u8; 32]); + let target = BytesN::from_array(&e, &[3u8; 32]); + let other_target = BytesN::from_array(&e, &[4u8; 32]); + + let base = statement( + &e, + &account, + &controller, + &Action::LostKey, + &cfg_hash, + Some(&target), + 7, + 100, + ); + let variants = [ + statement( + &e, + &other_account, + &controller, + &Action::LostKey, + &cfg_hash, + Some(&target), + 7, + 100, + ), + statement( + &e, + &account, + &other_controller, + &Action::LostKey, + &cfg_hash, + Some(&target), + 7, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::Compromise, + &cfg_hash, + Some(&target), + 7, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::LostKey, + &other_cfg_hash, + Some(&target), + 7, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::LostKey, + &cfg_hash, + Some(&other_target), + 7, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::LostKey, + &cfg_hash, + None, + 7, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::LostKey, + &cfg_hash, + Some(&target), + 8, + 100, + ), + statement( + &e, + &account, + &controller, + &Action::LostKey, + &cfg_hash, + Some(&target), + 7, + 101, + ), + ]; + for v in variants { + assert_ne!(base, v); + } + } +} diff --git a/docs/recovery/README.md b/docs/recovery/README.md index 2507a86..a310ab4 100644 --- a/docs/recovery/README.md +++ b/docs/recovery/README.md @@ -1,22 +1,25 @@ # Account recovery -Stage 4 of the perch recovery plan: schema, shared controller, compiler -integration, and client support for opt-in account recovery (guardian, ZK, -or combined), generalizing the validated Nido Stage 3 experiment into -`perch-ir`, `perch-doc-compiler`, `crates/perch-recovery`, and -`@stellar-registry/perch`. +Opt-in account recovery for perch smart accounts: restoring access after key +loss, or restoring an approved authorization baseline after suspected admin +compromise, without needing the account's own admin key. Recovery is +guardian-based, zero-knowledge-proof-based, or both combined, configured +per-account in the same reviewable policy document that already declares +signers and rules. -**Start here if you're reviewing this stage:** +**Start here:** -1. [`section-7-gate.md`](section-7-gate.md) — the open, release-blocking - decision this stage does not resolve. Read this first: nothing else here - should be read as answering it. +1. [`pending-activity-policy.md`](pending-activity-policy.md) — an + explicit, unresolved, release-blocking configuration decision this + change does not make. Read this first: nothing else here should be read + as resolving it. 2. [`schema.md`](schema.md) — the `recovery` document field: design, the - non-circular baseline commitment, and the canonical-form regression - guarantee for documents that don't use it. + non-circular baseline commitment, and the canonical-form guarantee for + documents that don't use it. 3. [`controller-governance.md`](controller-governance.md) — the shared - `perch-recovery` controller: Variant A completion, the `guard_apply_doc` - reconfigure gate, ZK adapter scope, and what's proven end-to-end. + `perch-recovery` controller: how completion authorizes `apply_doc`, the + `guard_apply_doc` reconfigure gate, ZK adapter scope, and what's proven + end-to-end. 4. [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md) — the constructorless/immutable requirement for the controller and any ZK verifier, and what "upgrade" means instead of code mutation. @@ -24,13 +27,17 @@ or combined), generalizing the validated Nido Stage 3 experiment into point that can change an account's rules or a controller's per-account state, in one table. 6. [`migration.md`](migration.md) — why an account deployed before this - stage can never gain recovery in place, and what moving to a new one + change can never gain recovery in place, and what moving to a new one actually requires. 7. [`formal-verification-impact.md`](formal-verification-impact.md) — Lean and `perch-conformance` impact: what needed no change and why, and what's explicitly scoped out with rationale rather than silently skipped. -See also the authoritative decision record this stage implements -(`/Users/willem/c/willemneal/firstmate/data/perch-zk-recovery-scout-p5/follow-up.md` -at dispatch time) and the validated experiment it generalizes (nido -`fm/nido-recovery-stage3-n8`, PR nidohq/nido#206). +A companion smart-account implementation (guardian/ZK/combined modes, the +same completion mechanism, real proofs) was built and exercised end-to-end +in a separate project before this change; see +[nidohq/nido#206](https://github.com/nidohq/nido/pull/206) for that prior +art. This change is not a port of it — the design differs in a few places +where perch's own schema and architecture allow something stronger (see +`controller-governance.md`'s opening section for exactly what and why) — but +everything needed to review *this* change is in this repository. diff --git a/docs/recovery/account-mutation-paths.md b/docs/recovery/account-mutation-paths.md index 6f38b96..9b61260 100644 --- a/docs/recovery/account-mutation-paths.md +++ b/docs/recovery/account-mutation-paths.md @@ -2,8 +2,8 @@ Every entry point that can change a `PerchAccount`'s stored authorization state (context rules, signers, applied `doc_hash`), or a recovery -controller's per-account state, as of this stage. This is the concrete -answer to "review... all account mutation paths." +controller's per-account state. This is the concrete answer to "review... +all account mutation paths." ## The account itself (`crates/perch-account`, `crates/perch-smart-account`) @@ -32,17 +32,28 @@ None of these mutate the *account's own* rule set directly — they mutate the controller's own per-account state, which then gates or is read by `apply_doc` (above). +`install`, `enforce`, and `guard_apply_doc` are exported contract functions +like any other on a deployed `PerchRecovery` instance — callable directly by +anyone, not only via OZ's real install flow or `perch-smart-account`'s +`apply_doc`. Each therefore starts with `.require_auth()`, +which — via Soroban's invoker-contract authorization — succeeds for free +when the caller genuinely is the account's own wasm making this exact +cross-call, and fails for a direct, unrelated caller. See `contract.rs`'s +doc comments on `install`/`complete`/`guard_apply_doc` for the full +reasoning. + | Entry point | What it changes | Authorization | |---|---|---| -| `Policy::install` | Writes the enrolled `CompiledRecoveryConfig` for an account. | Reachable only via `apply_doc`'s `add_context_rule` call (§ above); the actual authorization decision already happened in `guard_apply_doc` before this runs — see [`controller-governance.md`](controller-governance.md) for why `install` itself cannot be the gate. | -| `Policy::enforce` | Consumes a live, authorized, correctly-targeted attempt (marks `Completed`, revokes credentials, spends a nullifier). | Reachable only when the `"recovery"` context rule is selected for an `apply_doc` call — see `controller-governance.md`'s "Variant A completion." | +| `Policy::install` | Writes the enrolled `CompiledRecoveryConfig` for an account. | `smart_account.require_auth()` (see above). Reachable only via `apply_doc`'s own rule-(re)installation; the actual authorization *decision* already happened in `guard_apply_doc` before this runs — see [`controller-governance.md`](controller-governance.md) for why `install` itself cannot be the gate. | +| `Policy::enforce` | Consumes a live, authorized, correctly-targeted attempt (marks `Completed`, revokes credentials, spends a nullifier). | `smart_account.require_auth()` (see above), plus reachable only when the `"recovery"` context rule is selected for an `apply_doc` call — see `controller-governance.md`'s "Variant A completion." | | `Policy::uninstall` | Nothing (deliberate no-op). | N/A — see `controller-governance.md` for why. | -| `guard_apply_doc` | Nothing by itself (a check); refusing it blocks the `apply_doc` call that invoked it. | Called only from `perch-smart-account`'s `apply_doc`; internally requires guardian/ZK evidence when gating a `Protected` change. | -| `begin_lost_key_attempt` / `begin_compromise_attempt` | Creates or replaces the account's attempt. | Permissionless — declaring intent carries no authority (matches the validated experiment). `begin_compromise_attempt` additionally requires a baseline to be enrolled. | -| `submit_guardian_approval` | Adds to `Attempt::guardian_approvals`; may promote to `AuthorizedPending`. | The named guardian's own `require_auth()`; must be a member of the enrolled guardian set. | -| `submit_zk_proof` | Marks `Attempt::zk_verified`; may promote. | Permissionless — a verified proof is itself the authorization; the controller recomputes the statement from its own stored attempt, never trusting a caller-supplied one. | -| `submit_guardian_cancel` | Tallies a cancel vote (separate domain from initiation, §2.2); cancels once quorum is reached. | The named guardian's own `require_auth()`. | +| `guard_apply_doc` | Nothing by itself (a check); refusing it blocks the `apply_doc` call that invoked it. | `account.require_auth()` (see above); called only from `perch-smart-account`'s `apply_doc`, before it touches any context rule. Internally requires guardian/ZK evidence when gating a `Protected` change. | +| `begin_lost_key_attempt` / `begin_compromise_attempt` | Creates or replaces the account's attempt. | Permissionless — declaring intent carries no authority (matches a companion smart-account implementation's own validated design). `begin_compromise_attempt` additionally requires a baseline to be enrolled, and requires its target to equal that baseline exactly. | +| `submit_guardian_approval` | Adds to `Attempt::guardian_approvals`; may promote to `AuthorizedPending`. | The named guardian's `require_auth_for_args` over a digest binding this exact attempt (id, action, target, enrolled config) — not just the bare `(account, guardian)` arguments, so a signature can't be redirected to a different attempt. | +| `submit_zk_proof` | Marks `Attempt::zk_verified`; may promote. Reserves the proof's nullifier immediately (not deferred to completion). | Permissionless — a verified proof is itself the authorization; the controller recomputes the statement from its own stored attempt, never trusting a caller-supplied one. | +| `submit_guardian_cancel` | Tallies a cancel vote (a domain separate from initiation approval); cancels once the mode's cancellation evidence is complete. | The named guardian's `require_auth_for_args` over a digest binding this attempt and the `Cancel` action — the same binding requirement as `submit_guardian_approval`. | | `submit_zk_cancel` | Cancels on a valid proof over the `Cancel`-domain statement. | Permissionless, same rationale as `submit_zk_proof`. | +| `renew` | Nothing semantically — extends every one of an account's existing recovery-related persistent entries to the network's current maximum TTL. | Permissionless — see `controller-governance.md`'s "Keeping permanent state alive." | | `get_config`, `config_hash`, `get_attempt`, `has_pending` | Nothing (read-only). | None. | ## What this means for the review diff --git a/docs/recovery/controller-governance.md b/docs/recovery/controller-governance.md index c610147..03f30b2 100644 --- a/docs/recovery/controller-governance.md +++ b/docs/recovery/controller-governance.md @@ -1,51 +1,51 @@ # Recovery controller: design, governance, and upgrade policy -This documents `crates/perch-recovery`: what it generalizes from the -validated Nido Stage 3 experiment, how completion actually authorizes -`apply_doc` (Variant A), the reconfigure-authorization rule that governs -every change to enrolled `Protected` recovery, and how the controller itself -is governed. +This documents `crates/perch-recovery`: how completion authorizes +`apply_doc`, the reconfigure-authorization rule that governs every change to +enrolled `Protected` recovery, and how the controller itself is governed. -## What's generalized from the validated experiment, and what's changed +## Design choices, and why -Nido's Stage 3 controller validated the shape this crate implements: -initiation/delay/expiry/cancel/completion state machine, guardian and ZK -evidence against one frozen proposal, `config_hash` commitment, and Variant A -completion (recovery authorizes the account's own document-apply operation -rather than a dedicated raw mutator). `crates/perch-recovery` keeps all of -that. Three things are deliberately different, each for a stated reason: +A companion smart-account implementation +([nidohq/nido#206](https://github.com/nidohq/nido/pull/206)) validated the +overall shape this crate implements: an initiation/delay/expiry/cancel/completion +state machine, guardian and ZK evidence checked against one frozen proposal, +a `config_hash` commitment, and completion by authorizing the account's own +document-apply operation rather than a dedicated raw mutator ("Variant A" +below). `crates/perch-recovery` keeps that overall shape, with three +deliberate differences: 1. **Enrollment happens through the document, not a side-channel `enroll` - call.** Nido's controller predates perch's schema support for recovery - (§3 of the follow-up review): it received `RecoveryConfig` as a direct - call argument because there was nowhere else for it to live. Now that - `perch-ir`/`perch-doc-compiler` carry it (see - [`schema.md`](schema.md)), enrollment and reconfiguration are just what - happens when `apply_doc` installs a compiled document whose `recovery` - section differs from before — the OZ `Policy::install` hook receives the - compiled config as its install params, the same mechanism every other - perch policy (the interpreter, `spending_limit`) already uses. -2. **`Protected` reconfigure is general, not additive-only.** Nido's - `reconfigure` entry point accepted exactly two transitions - (`GuardianOnly→Combined`, `ZkOnly→Combined`) and rejected everything else, - including under `Loss` — a scope decision for their bounded experiment, - not a requirement from the authoritative decision record. That record's - §2 states plainly: *"Protected configuration changes: Require ordinary - admin authorization plus the enrolled recovery condition,"* with no - restriction to additive transitions, and §2.1: *"A downgrade uses the - current, stronger requirements"* — implying a downgrade is possible, not - forbidden. `guard_apply_doc` (below) implements the general rule; the two - additive transitions Nido validated remain reachable as the common case - (they're just ordinary instances of "config changed while `Protected`," - authorized by guardian quorum with no new cryptography needed), and the - guardian-authorized path is fully general with no extra risk. The - ZK-authorized path reuses the same statement-hashing approach used for - initiation (a new domain tag, `Action::Reconfigure`, over the same kind of - 32-byte digest) — not a new cryptographic primitive, so there was no - reason to inherit Nido's narrower "no Reconfigure-domain circuit exists - yet" limitation, which was about a *specific* circuit they'd built and - tested, not an interface constraint. See "ZK adapter scope" below for what - *is* still deliberately not shipped. + call.** That companion implementation received `RecoveryConfig` as a + direct call argument, because its target account contract's document + schema had no field for it. Perch's own schema now carries it (see + [`schema.md`](schema.md)), so enrollment and reconfiguration are just + what happens when `apply_doc` installs a compiled document whose + `recovery` section differs from before — the OZ `Policy::install` hook + receives the compiled config as its install params, the same mechanism + every other perch policy (the interpreter, `spending_limit`) already + uses. +2. **`Protected` reconfigure is general, not additive-only.** That + companion implementation's `reconfigure` entry point accepted exactly two + transitions (`GuardianOnly→Combined`, `ZkOnly→Combined`) and rejected + everything else, including under `Loss` — a scope decision for its own + bounded experiment. Here, a `Protected` account requires ordinary admin + authorization *plus* the currently enrolled recovery condition to change + or disable recovery — with no restriction to additive transitions, and a + downgrade is possible (using the *current*, stronger requirements), not + forbidden. `guard_apply_doc` (below) implements that general rule; the + two additive transitions the companion implementation validated remain + reachable as the common case (they're just ordinary instances of "config + changed while `Protected`," authorized by guardian quorum with no new + cryptography needed), and the guardian-authorized path is fully general + with no extra risk. The ZK-authorized path reuses the same + statement-hashing approach used for initiation (a new domain tag, + `Action::Reconfigure`, over the same kind of 32-byte digest) — not a new + cryptographic primitive, so there was no reason to inherit the narrower + "no reconfigure-domain circuit exists yet" limitation the companion + implementation had, which was about a *specific* circuit it had built + and tested, not an interface constraint. See "ZK adapter scope" below for + what *is* still deliberately not shipped. 3. **No circuit.** See "ZK adapter scope" below. ## Variant A completion, precisely @@ -99,20 +99,80 @@ same mechanism the host uses for a real `__check_auth` invocation. body. If the document fails to compile or install (any `DocCompilerError`, the anti-brick check), the whole transaction reverts — including the attempt-consuming mutation above — so there is no reachable state where the -attempt is spent but the account's rules are unchanged. This closes the -follow-up review §3.1 gap by construction (no ledger-scoped grant to leave -stale or ungated) rather than by hardening a fragile flag. +attempt is spent but the account's rules are unchanged. A ledger-scoped +completion grant that merely records "a completion happened this ledger," +without binding *which* mutation it authorized, would not give this +guarantee; this design avoids that class of gap by construction rather than +by hardening a fragile flag. + +## Caller authentication on `install`, `enforce`, and `guard_apply_doc` + +All three are exported contract functions on a deployed `PerchRecovery` +instance, reachable by a direct call from anyone — not only via OZ's real +install flow or `perch-smart-account`'s `apply_doc`. Each therefore starts +with `.require_auth()`: + +- **`install`** — without it, anyone could call it directly for an arbitrary + `smart_account` address, overwriting that account's enrolled config with + attacker-chosen guardians/verifier/profile, entirely bypassing + `guard_apply_doc`'s reconfiguration gate (which never runs for a direct + `install` call). +- **`enforce`** (via `complete`) — `context: Context` is an ordinary function + argument the caller fully controls; nothing about receiving a + `Context::Contract` value proves it reflects what the host is actually + authorizing. Without the gate, anyone who knows or reconstructs a pending + attempt's target document bytes could call `enforce` directly with a + forged context, consuming the attempt (revoking credentials, spending its + nullifier) without `apply_doc`'s body ever running. +- **`guard_apply_doc`** — without it, anyone who obtains valid reconfigure + evidence (for example by observing the real `apply_doc` transaction before + it lands) could call this entry point standalone, burning a one-time ZK + nullifier or a guardian's signature with no document ever changing: + front-running and denial-of-service against the real reconfiguration. + +Each succeeds for free on its real path and fails for a direct, unrelated +caller, via Soroban's invoker-contract authorization — the host's documented +first-checked path for `require_auth`: *"if contract C invokes contract D, +then C authorized D... requires no credentials as the host literally +observes the call from C to D"* (`soroban-env-host`'s `auth` module docs). +On the real path, `smart_account`'s own wasm is the direct invoker of each of +these cross-calls (OZ's `do_check_auth` invokes `enforce`; `apply_doc` +invokes `guard_apply_doc` before touching any context rule and invokes +`install` while re-installing its rules), so the check passes without +needing a signature — it is not in tension with `enforce` running under a +zero-signer context rule. A direct call from anywhere else has no such +invoker relationship and is rejected before touching storage. This is +verified directly against the pinned `soroban-env-host`'s own +`require_auth_internal`/`maybe_check_invoker_contract_auth` (invoker checked +first, unconditionally, before the account-authorization-tracker path that a +custom account's own recursive `__check_auth` reentry would otherwise +conflict with). + +## Binding guardian evidence to a specific attempt + +`submit_guardian_approval` and `submit_guardian_cancel` authenticate a digest +— built by `zk::statement` from `(account, controller, action, config_hash, +target_or_removal, attempt_id, delay_ledgers)` — via +`require_auth_for_args`, the same statement shape (and, for approval, the +same statement) a ZK initiation or cancellation proof is verified against. +Authenticating only the bare `(account, guardian)` call arguments, as an +earlier version of this crate did, would let a guardian's signature be +delayed and later consumed for a *different* attempt than the one they +actually approved — the account could declare a fresh attempt after the +first was cancelled or expired, and a stale but still-valid signature over +just `(account, guardian)` would satisfy it. Binding to the attempt's id, +action, and target closes that: a signature is only ever valid for the exact +attempt the guardian saw. ## The `guard_apply_doc` gate and why it — not `install`/`uninstall` — is authoritative `stellar_accounts::smart_account::storage::remove_context_rule` calls a policy's `uninstall` via `try_uninstall` and **discards the result even if -it panics** (confirmed by reading the pinned dependency directly — this is -exactly the class of gap the follow-up review §3.2 flags in a different -codebase). Since `apply_doc` wipes and reinstalls the *entire* rule set on -every call, a policy that tried to block a `Protected` account's -reconfiguration from inside `uninstall` would simply be ignored, and the -rule would be removed anyway. +it panics** (confirmed by reading the pinned dependency directly). Since +`apply_doc` wipes and reinstalls the *entire* rule set on every call, a +policy that tried to block a `Protected` account's reconfiguration from +inside `uninstall` would simply be ignored, and the rule would be removed +anyway. The actual gate is `perch-smart-account`'s `apply_doc`, which — whenever `PerchStorage::recovery_controller` names a currently-adopted instance — @@ -120,20 +180,21 @@ cross-calls that instance's `guard_apply_doc(account, new_recovery, recovery_evidence)` **before** touching any context rule: 1. **Unconditional:** if a live attempt exists (`CollectingEvidence`, or - `AuthorizedPending` and not yet expired), refuse. This is property 9 - (configuration consistency), independent of §7's still-open freeze/continue - question — see [`section-7-gate.md`](section-7-gate.md) — and it is what - makes `apply_doc`'s own timing "for free" for completion: `enforce` (§ - above) already flips a legitimate completion's attempt to `Completed` - *before* `guard_apply_doc` runs, so this check reads `false` for exactly - that call and blocks every other concurrent or racing call. + `AuthorizedPending` and not yet expired), refuse. This has to hold + regardless of whether the account's chosen pending-activity policy + applies here (see [`pending-activity-policy.md`](pending-activity-policy.md) + for that separate, still-open question) — it's what makes `apply_doc`'s + own timing "for free" for completion: `enforce` (above) already flips a + legitimate completion's attempt to `Completed` *before* `guard_apply_doc` + runs, so this check reads `false` for exactly that call and blocks every + other concurrent or racing call. 2. **No change, or no prior enrollment:** always fine — establishing new protection, or reapplying an unchanged configuration (including as a side effect of a legitimate completion, whose target document carries the same `recovery` section as before), needs nothing extra. 3. **Currently `Loss`:** ordinary admin authorization (already established by `apply_doc`'s own `require_auth`) is sufficient for *any* new - configuration, including removing it — §2.1. + configuration, including removing it. 4. **Currently `Protected`:** requires the *currently* enrolled condition's evidence over a digest binding `(account, controller, Reconfigure, old_config_hash, new_config_hash_or_removal)` — guardian quorum via @@ -150,20 +211,97 @@ the rule it's attached to and writes the config (the gate already ran); ## ZK adapter scope `crates/perch-recovery/src/zk.rs` fixes the **statement** a proof must be -over (network, account, controller, action, config hash, target-or-removal, -attempt nonce, delay — every field the follow-up review §5.3 names for a -proposal commitment) and a minimal, proof-system-agnostic verifier interface -(`verify_proof(statement, nullifier, proof, pool) -> bool`). It ships **no -circuit** and is not tested against one. This is deliberate, per §5.4: -*"Proof-system portability is an interface goal; supporting multiple proof -systems in the first release is not a requirement."* Building and -validating an actual circuit is real cryptographic work with its own review -needs, out of proportion to a schema-and-controller stage; the interface is -designed so a future circuit can be dropped in (any verifier satisfying this -trait) without changing the controller. `ZkOnly`/`Combined` modes are fully -represented in storage, compiled correctly, and gated identically to -`GuardianOnly` — only the "does a real ZK circuit exist to generate proofs -against" question is out of scope here, tracked as follow-up work. +over — network, account, controller, action, config hash, target-or-removal, +attempt nonce, delay — and a minimal, proof-system-agnostic verifier +interface (`verify_proof(statement, nullifier, proof, pool) -> bool`). It +ships **no circuit** and is not tested against one. Building and validating +an actual circuit is real cryptographic work with its own review needs, +out of proportion to this change; the interface is designed so a future +circuit can be dropped in (any verifier satisfying this trait) without +changing the controller. `ZkOnly`/`Combined` modes are fully represented in +storage, compiled correctly, and gated identically to `GuardianOnly` — only +"does a real ZK circuit exist to generate proofs against" is out of scope +here, tracked as follow-up work. + +## Bounding permissionless evidence collection + +`begin_lost_key_attempt`/`begin_compromise_attempt` are permissionless — +declaring intent carries no authority, so nothing gates who may call them. +Without a bound on how long the resulting attempt may sit in +`CollectingEvidence`, a single such call would block every ordinary +`apply_doc` indefinitely (via `guard_apply_doc`'s unconditional live-attempt +check) if the enrolled mode's evidence simply never arrives — a +permissionless, no-cost denial-of-service against the account's own ordinary +administration. `Attempt::evidence_deadline`, set at `begin_attempt` to +`created_at + expiry_ledgers` (the account's own configured recovery-process +budget, reused rather than adding a second schema field for the same kind of +window), bounds this: `is_live` treats a `CollectingEvidence` attempt as +no-longer-live once its deadline passes, at which point `guard_apply_doc` +stops blocking, and a fresh attempt can replace it. + +## What the commitment does, and does not, verify + +`target_doc_hash` and `config_hash` are content-addressed commitments — +`sha256` of specific bytes — checked for exact equality. They prove the +account applies *exactly the document that was committed to*, byte for byte. +They do **not** parse or inspect that document's contents on-chain: the +controller has no JSON parser and no `perch-ir` dependency in its deployable +build, by design (it stays a small, auditable piece of logic with no +document-schema coupling beyond the wire types `perch-doc-compiler` already +produces). + +Two consequences follow, and are evidence-provider/reviewer responsibilities +rather than on-chain-enforced properties: + +- **A compromise-recovery target must actually be the approved baseline's + content** — the controller only checks that `target_doc_hash` equals the + enrolled `baseline.doc_hash` (see `begin_compromise_attempt`); it cannot + independently verify what that baseline hash "means" beyond having been + declared at enrollment. Reviewing that a declared baseline hash genuinely + names a real, previously-approved document is part of enrollment review — + the same way reviewing that a `Scope::Contract` address is the intended + contract is part of ordinary rule review (see + [`schema.md`](schema.md)'s "Non-circular baseline commitment"). +- **A target document's actual content should only replace the credentials + named in `replaced_credentials`, and should not reintroduce a previously + revoked credential** — the controller checks that the caller's *declared* + `replaced_credentials` are a subset of the enrolled `replaceable` set at + attempt creation, but it cannot inspect the target document's actual + signer list at completion time to confirm the applied document's real + diff matches that declaration, or that it avoids every credential in the + account's permanent `revoked` set. Guardians and ZK-circuit designers are + the parties who see the actual target document before approving or + proving against its hash; that review — confirming the document only + changes what it claims to, and does not resurrect a revoked credential — + is where this property is enforced, not on-chain. Whoever operates a + recovery-enrolled account should treat "the evidence provider verified the + target document's actual diff before signing" as part of what a guardian's + or prover's approval means, the same way any multisig signer is expected + to review what they're signing before approving it — not something the + hash commitment can substitute for. + +## Keeping permanent state alive + +Soroban persistent storage entries have a finite maximum TTL (`Env::storage() +.max_ttl()`) — there is no "forever" setting. The controller extends the +relevant entries to that current maximum on every write, and additionally on +read in the highest-traffic path (`require_config`, used by every +begin/approve/proof/cancel call) and inside `guard_apply_doc` (every +`apply_doc` call on an enrolled account) — but an entry nothing ever touches +again (an enrolled but otherwise-idle account's config, revoked set, or a +long-completed attempt's spent nullifier) will still eventually expire +absent some renewal. + +`PerchRecovery::renew(account)` is the explicit, permissionless keep-alive +for exactly that gap: it extends every one of `account`'s existing recovery +entries (config, current/most recent attempt and its nullifier if spent, +revoked set, and the lifetime counters) to the current maximum. It changes +nothing about what any of that state means — it only ever extends TTL — so +anyone (a keeper script, a wallet's own periodic background job) can call it +safely and without authorization. An integration relying on recovery staying +available (or on revocation staying permanent) through long account +inactivity should call `renew` periodically; this is a genuine platform +constraint recovery inherits, not a gap specific to this controller's logic. ## Controller governance: no admin, ever @@ -201,7 +339,7 @@ prove): - `Combined`-mode cancellation requires BOTH a guardian quorum AND a valid ZK cancellation proof for the same attempt — guardian quorum alone does not cancel, a ZK proof alone does not cancel, and only once both factors are - present does the attempt cancel (§2.2). + present does the attempt cancel. - A cancelled attempt allows a fresh one afterward. - A cancellation factor (guardian or ZK) arriving after an attempt has already completed is refused, not silently accepted — a completed attempt @@ -211,6 +349,22 @@ prove): nullifier is released. - A `Protected` reconfiguration (here: disabling recovery entirely) with ordinary admin authorization alone is refused. +- A suspected-compromise attempt whose target does not equal the enrolled + baseline is refused; the actual baseline is accepted. +- A `CollectingEvidence` attempt stops blocking ordinary `apply_doc` calls + once its evidence deadline elapses, and a fresh attempt can then replace + it. +- A ZK initiation proof's nullifier is reserved immediately: reusing it for + a *different* statement (the cancellation domain, on the same attempt) is + refused before either use completes — not only once one of them does. +- Fingerprinting a signer's credential is hex-case-insensitive: re-declaring + the identical physical key under different hex casing resolves to the same + fingerprint. + +`crates/perch-recovery/src/zk.rs`'s own unit test pins down that +`statement()` — the digest guardian signatures and ZK proofs are bound to — +actually varies with every one of account/controller/action/config/target/ +attempt-id/delay, independent of any authorization mocking. `crates/perch-ir/tests/recovery.rs` and `crates/perch-recovery` (unit-level, via `perch_doc_compiler` types) cover the schema/wire side: guardian-only @@ -222,7 +376,12 @@ Not yet covered (tracked, not silently assumed sound): a live ZK circuit exercising `submit_zk_proof`/`submit_zk_cancel` against a real verifier (no circuit is shipped — see "ZK adapter scope"); a rule-teardown scenario where `uninstall` itself panics (this crate's `uninstall` is a no-op by design, so -the property to check is narrower than Nido's — that removal always -succeeds regardless — which follows directly from `uninstall` never being -able to fail); and the full §7 pending-activity enforcement beyond policy -mutation (see [`section-7-gate.md`](section-7-gate.md)). +the property to check is narrower — that removal always succeeds regardless +— which follows directly from `uninstall` never being able to fail); the +full pending-activity enforcement beyond blocking conflicting document +changes (see [`pending-activity-policy.md`](pending-activity-policy.md)); +and a live rejection of a direct, unauthenticated call to `install`/ +`enforce`/`guard_apply_doc` under genuinely enforcing (non-mocked) +authorization — the existing suite runs under mocked auth throughout (see +"Caller authentication" above for the reasoning verified instead against the +pinned host source directly). diff --git a/docs/recovery/formal-verification-impact.md b/docs/recovery/formal-verification-impact.md index 93aab81..1649f1e 100644 --- a/docs/recovery/formal-verification-impact.md +++ b/docs/recovery/formal-verification-impact.md @@ -13,9 +13,9 @@ Lean `Semantics.lean`/`Lowering.lean`/`Theorems.lean` (T1–T6) model **perch-program evaluation** — the RPN machine `__check_auth` runs per invocation. `RecoveryConfig` never lowers to a perch-program op: it is not installed as, or referenced by, any interpreter program. This isn't an -oversight; it's the load-bearing design decision from the follow-up review -§1 and §5.1 ("Recovery state remains outside the stateless interpreter"), -and it is *why* recovery is architecturally possible at all — +oversight; it's a load-bearing design decision — recovery state stays +outside the stateless interpreter entirely — and it is *why* recovery is +architecturally possible at all — `docs/verification/THEORY.md`'s enforceability argument states plainly that no execution monitor, including perch's, can enforce "the account can always recover" (a liveness property), and that perch "addresses the @@ -26,7 +26,7 @@ perch's own enforceable-fragment boundary — it was never a candidate for inclusion in the per-invocation-safety-property model in the first place. Concretely: `crates/perch-compile` (the crate that lowers `PolicyDoc` rules -into perch-program `InstallParams`) is untouched by this stage — recovery +into perch-program `InstallParams`) is untouched by adding recovery — lowering happens entirely inside `perch-doc-compiler`, mapping `perch_ir::RecoveryConfig` directly to a new wire type (`CompiledRecoveryConfig`) with no perch-program involvement. So T1–T6 and @@ -46,7 +46,7 @@ engineering: a new sum-of-products shape enters the emitter and its verified inverse parser, and `emitDoc_injective`'s proof structure would need new cases throughout. -**This stage does not do that work**, for two reasons stated plainly rather +**This change does not do that work**, for two reasons stated plainly rather than hidden: 1. **Correctness bar.** `formal/README.md` states every theorem here is @@ -70,7 +70,7 @@ than hidden: replay) is unaffected and continues to pass, because it replays `testdata/eval/eval-vectors.json` and round-trips the existing (recovery-absent) `ci-publish*.canonical.json` fixtures — none of which -gained a `recovery` field. The two new fixtures added by this stage +gained a `recovery` field. The two new fixtures this change adds (`ci-publish-recovery.json`, `ci-publish-recovery-combined.json`) are **not** round-tripped through the Lean model, because Lean's `Doc` cannot represent them yet. Their canonical-form and hash agreement is instead @@ -80,11 +80,11 @@ by the Rust (`crates/perch-ir/tests/recovery.rs`) and TypeScript bytes — which is real cross-implementation conformance, just not machine-checked against the Lean model. -## Tracked follow-up (not this stage) +## Tracked follow-up Extending `Canon.lean`/`CanonProofs.lean` to cover `RecoveryConfig` and re-establishing `emitDoc_injective` over the enlarged domain is legitimate -future work, tracked here rather than attempted under this stage's time -budget. It should be undertaken as its own reviewed change with room to get -the injectivity argument right, not bundled into a schema-and-controller +future work, tracked here rather than attempted as part of this change. It +should be undertaken as its own reviewed change with room to get the +injectivity argument right, not bundled into a schema-and-controller release. diff --git a/docs/recovery/migration.md b/docs/recovery/migration.md index 6facf10..98937f4 100644 --- a/docs/recovery/migration.md +++ b/docs/recovery/migration.md @@ -4,10 +4,10 @@ already-deployed `perch-account` can gain recovery support in place, or needs a new account. -**Hard rule this document follows throughout:** shipping this release does -not, and cannot, change the behavior of any already-deployed `PerchAccount` -or already-deployed `perch-doc-compiler` instance. Both are constructorless -and immutable by construction (see +**Hard rule this document follows throughout:** shipping recovery support +does not, and cannot, change the behavior of any already-deployed +`PerchAccount` or already-deployed `perch-doc-compiler` instance. Both are +constructorless and immutable by construction (see [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md)). Nothing below is a claim that a future library release changes deployed accounts — it is the opposite claim, worked out to its consequences. @@ -40,29 +40,32 @@ execution entry point: replacing it means deploying a new account and moving Two independent immutable artifacts must both understand recovery for enrollment to be possible at all: -1. **The doc-compiler instance the account calls.** A pre-Stage-4 compiler's - own statically-linked `perch-ir` has no `recovery` field in its +1. **The doc-compiler instance the account calls.** A compiler predating + recovery support has its own statically-linked `perch-ir` with no + `recovery` field in its `deny_unknown_fields` list (`crates/perch-ir/src/parse.rs`) — a document containing a `"recovery"` key is rejected as an unknown field, on-chain, before anything else happens. This is not a permissions error; the old compiler genuinely does not know the shape exists. 2. **The account's own `apply_doc` logic.** Even if a document could get - past compilation, a pre-Stage-4 account's `apply_doc` has no code path - that calls a recovery controller — that call only exists in a - post-Stage-4 build of `perch-smart-account`/`perch-account`. + past compilation, an account built before recovery support existed has + an `apply_doc` with no code path that calls a recovery controller — that + call only exists in a build of `perch-smart-account`/`perch-account` + with recovery support. -Consequently: **a `PerchAccount` deployed before this release can never -enroll recovery through its own `apply_doc`, under any circumstances.** -This is a permanent property of that deployment, not a temporary gap this -document works around. +Consequently: **a `PerchAccount` deployed before recovery support existed +can never enroll recovery through its own `apply_doc`, under any +circumstances.** This is a permanent property of that deployment, not a +temporary gap this document works around. ## The two cases -### Case A — account built before this release +### Case A — account built before recovery support existed Recovery is permanently unavailable in place. The only path is: -1. Deploy a new `PerchAccount` from a post-Stage-4 build. Its constructor +1. Deploy a new `PerchAccount` from a build with recovery support. Its + constructor takes the same shape as before (`admin_signers: Vec`) — the user's existing signer keys/credentials are reused as-is; nothing about a WebAuthn credential, ed25519 key, or delegated address is @@ -100,13 +103,13 @@ Recovery is permanently unavailable in place. The only path is: which `perch-account`/`perch-doc-compiler` build an account was deployed against, a wallet cannot currently *discover* whether a given account is Case A or Case B by inspecting the account alone. Until perch grows a -queryable version marker (tracked as follow-up work, not part of this -release), integrators should record the release each account was deployed +queryable version marker (tracked as follow-up work), integrators should +record the release each account was deployed from at deploy time (e.g. alongside however they already track deployments — see `DEPLOYED.md`-style records used elsewhere in this repo's tooling) and consult that record rather than guessing. -### Case B — account built from this release or later, recovery not yet enrolled +### Case B — account built with recovery support, not yet enrolled No migration is needed. Recovery is enrolled, changed, or removed the same way any other policy change is made: submit a new document via `apply_doc`. @@ -125,8 +128,8 @@ shown above, portable as-is. ## What this document does not claim -- It does not claim perch can make a pre-Stage-4 account upgradeable after - the fact. It cannot, by design (see +- It does not claim perch can make an account built before recovery + support existed upgradeable after the fact. It cannot, by design (see [`vk-and-controller-immutability.md`](vk-and-controller-immutability.md)). - It does not claim balance or external-reference migration is automated by perch tooling. Both are explicit, integration-specific steps an diff --git a/docs/recovery/pending-activity-policy.md b/docs/recovery/pending-activity-policy.md new file mode 100644 index 0000000..edaa815 --- /dev/null +++ b/docs/recovery/pending-activity-policy.md @@ -0,0 +1,94 @@ +# Open decision: activity during a pending recovery attempt (release-blocking) + +**Status: OPEN. This is a release-blocking gate, not a design note.** No +recovery-enabled account should be deployed to production, and no +integration should advertise recovery as safe to rely on, until this +decision is made explicitly by whoever operates the account or protocol — +not inferred from this codebase, not defaulted by the library, and not +resolved by this change. + +## The question + +While a recovery attempt is pending — from the moment its evidence is +satisfied until it completes, is cancelled, or expires — should the +account's *ordinary*, admin-authorized activity (calling other contracts, +moving funds) continue, or be frozen? And should lost-key and +suspected-compromise attempts behave differently on this axis? + +This is deliberately unresolved, and the schema encodes that: an account +enrolling recovery must name `pending_activity` explicitly +(`RecoveryConfig::pending_activity`, `crates/perch-ir/src/doc.rs`) as either +`Freeze` or `Continue` — there is no default, and no way to enroll recovery +without making this choice. A third candidate, restricting only *selected* +operations rather than all-or-nothing, was considered and is **not +modeled** — it needs an explicit capability-boundary design this change does +not attempt, so it's left out entirely rather than half-built. + +Each candidate has a real cost: + +- **Freeze everything.** Limits what a compromised admin can do during the + delay window, at the cost of the account's own availability for that + window — including for the legitimate owner, if the attempt turns out to + be spurious or contested. +- **Continue everything.** Preserves availability, at the cost that a + genuinely compromised admin can keep acting — including moving funds or + changing external state — for the entire delay window before recovery + takes effect. +- **Restrict selectively** (not modeled here). Could in principle preserve + more of both properties, but requires deciding *which* operations are safe + to continue and building the machinery to distinguish them — real design + and implementation work this change does not do. + +Do not infer an answer from any of: the delay/expiry timing values an +account chooses, the `Protected` profile's name, or the fact that this +document is otherwise complete. None of those settle the tradeoff above. + +## What is, and is not, already enforced + +- **Blocking a *conflicting policy-document change* while an attempt is + pending is a separate, already-decided property — not part of this open + question.** The controller refuses any `apply_doc` call not authorized by + the recovery evidence itself while an attempt is pending (see + [`controller-governance.md`](controller-governance.md)), regardless of the + enrolled `pending_activity` value. This has to hold unconditionally: + without it, an admin could race a pending attempt with a conflicting + document change, silently reinterpreting what the attempt was approved + against. `pending_activity` governs something narrower and genuinely + open: **ordinary contract calls the account authorizes that are not a + policy-document change at all** (moving funds, calling another contract) + — the freeze/continue tradeoff described above. +- **`Freeze`/`Continue` are recorded and queryable, not mechanically + enforced end to end.** The controller exposes the pending attempt's state + and the enrolled policy so an integration can check it. Actually + *enforcing* `Freeze` against arbitrary non-recovery context rules would + require threading a pending-recovery check through the interpreter's + context-rule evaluation for every rule on the account, not only the + recovery-authorizing one — a cross-cutting change to the interpreter's + evaluation path that this change does not make. Until that exists, an + account enrolled with `Freeze` records that intent reviewably, but does + not yet have it mechanically enforced against every other rule on the + account. There is little point building that enforcement machinery before + the tradeoff above is actually decided, since the answer shapes what needs + building. + +## What must happen before production reliance + +1. Whoever operates the account/protocol decides: freeze, continue, + restrict (which would first need its own design), or some other + precisely-defined rule — including the related question of what happens + to a *stale lost-key source document* if an ordinary policy write is + attempted while an attempt targeting an older snapshot is pending (block + the write, invalidate the attempt, or another explicit conflict rule — + this change does not silently choose overwrite-or-merge behavior for + that case; see [`schema.md`](schema.md) for how a lost-key target is + constructed). +2. If the decision requires enforcement beyond what's already unconditional + (blocking conflicting document changes), that enforcement is designed and + implemented as its own reviewed change. +3. Only after both of the above should `Protected`-profile, + recovery-enrolled accounts be treated as production-ready for resisting a + compromised admin specifically during the pending window. + +Until then: this schema field and the controller's current behavior are +available for experimentation, review, and further design work — not for a +production deployment relying on the pending-activity guarantee to hold. diff --git a/docs/recovery/schema.md b/docs/recovery/schema.md index 918e7a6..56e6889 100644 --- a/docs/recovery/schema.md +++ b/docs/recovery/schema.md @@ -1,11 +1,9 @@ # Recovery configuration: schema design This documents the `recovery` field added to `PolicyDoc` -(`crates/perch-ir/src/doc.rs`), why it is shaped the way it is, and how it -was fit into `CANONICAL.md` without disturbing the canonical form of any -document that doesn't use it. See the authoritative decision record's §5.5 -("Reviewable configuration without circular hashes") for the requirements -this design answers. +(`crates/perch-ir/src/doc.rs`): why it's shaped the way it is, and how it +fits into `CANONICAL.md` without disturbing the canonical form of any +document that doesn't use it. ## What's in scope here, and what isn't @@ -30,65 +28,76 @@ the single fact that keeps every pre-existing document's hash unchanged. ``` RecoveryConfig { - profile: RecoveryProfile, // Loss | Protected — §2.1 + profile: RecoveryProfile, // Loss | Protected mode: RecoveryMode, // GuardianOnly | ZkOnly | Combined controller: String, // adopted controller instance's address baseline: Option,// required to enroll compromise recovery - replaceable: Vec, // signer ids recovery may replace — §4.1 + replaceable: Vec, // signer ids recovery may replace delay_ledgers: u32, // timelock window expiry_ledgers: u32, // authorized-attempt lapse window max_cancels: u32, // lifetime cancellation cap (griefing bound) - pending_activity: PendingActivityPolicy, // Freeze | Continue, NO default — §7 + pending_activity: PendingActivityPolicy, // Freeze | Continue, NO default } ``` -- **`profile` and `mode` are orthogonal fields**, exactly per §2.1 ("Either - profile can use any of the three authentication modes"). Keeping them as - two independent fields rather than a combined enum avoids a 6-way +- **`profile` and `mode` are orthogonal fields.** Either recovery mode + (guardian-only, ZK-only, combined) can pair with either profile + (`Loss`: ordinary admin can change or disable recovery on its own; + `Protected`: changing or disabling recovery additionally needs the + currently-enrolled recovery condition — see + [`controller-governance.md`](controller-governance.md)). Keeping them as + two independent fields rather than a combined enum avoids a six-way cross-product type for no benefit. - **`mode` is a per-variant enum, not a flattened struct with optional ZK - fields.** The validated experiment this stage generalizes stored mode as - one struct with `guardians: Vec
` and `verifier: Option
` - side by side (`RecoveryConfig` in that codebase's `types.rs`), because its - target SDK's `#[contracttype]` derive doesn't support `Option` - on a flattened struct field cleanly. Perch's document model has no such - constraint (`perch-ir` is plain Rust, not itself a `#[contracttype]`), so - `RecoveryMode::GuardianOnly(GuardianSet)` simply **cannot** carry a - `verifier`/`circuit-id`/`pool` field at all — "guardian-only requires no ZK - machinery" is enforced by the type, not by a runtime check that a ZK field - happens to be `None`. The wire (`perch-doc-compiler`) and JSON - (`perch-js`) encodings flatten the tagged variant's fields into one object - at the wire level (`{"type":"guardian-only","guardians":[...],"quorum":N}`) - for a compact document, but the *source of truth* — the Rust and TS types - — stay per-variant. + fields.** A companion smart-account implementation + ([nidohq/nido#206](https://github.com/nidohq/nido/pull/206)) stored mode + as one struct with `guardians: Vec
` and `verifier: Option
` + side by side, because its target SDK's `#[contracttype]` derive doesn't + support `Option` on a flattened struct field cleanly. + Perch's document model has no such constraint (`perch-ir` is plain Rust, + not itself a `#[contracttype]`), so `RecoveryMode::GuardianOnly(GuardianSet)` + simply **cannot** carry a `verifier`/`circuit-id`/`pool` field at all — + guardian-only recovery requiring no ZK machinery is enforced by the type, + not by a runtime check that a ZK field happens to be `None`. The wire + (`perch-doc-compiler`) and JSON (`perch-js`) encodings flatten the tagged + variant's fields into one object at the wire level + (`{"type":"guardian-only","guardians":[...],"quorum":N}`) for a compact + document, but the *source of truth* — the Rust and TS types — stay + per-variant. - **`delay_ledgers`/`expiry_ledgers` are ledger-sequence deltas, not durations in seconds.** This matches perch's own existing convention - (`Rule::not_after_ledger`) rather than the validated experiment's - timestamp-based fields — the account, interpreter, and every other + (`Rule::not_after_ledger`) — the account, interpreter, and every other ledger-relative quantity in this schema already speaks ledger sequence, and mixing units within one document would be a real (if subtle) foot-gun for anyone reading it. -- **`replaceable` is required non-empty**, per §4.1: "Enrollment records... - identifies which signer entries or roles recovery may replace." An empty - list would enroll recovery that can never restore access — rejected at - validation, not left as a silent no-op. In the document it names - document-local signer **ids** (human-reviewable, e.g. `"admin"`); the - compiler resolves each to a `sha256` fingerprint of that signer's actual - credential (verifier+key, or the delegated address) for the wire form the - controller stores — not the id string, which a later document could - legitimately reuse for a different physical key. This is what lets - revocation (property 10: an old baseline must not restore a revoked - credential) survive id reuse across documents. -- **`baseline` is optional, and its presence is what gates suspected-compromise - recovery**, per §4.2: lost-key recovery needs no predetermined baseline - (it targets "current approved document with designated credentials - replaced"); compromise recovery needs one to restore to. `None` therefore - means "lost-key recovery only," not "recovery is half-configured." -- **`pending_activity` has no default and no third "unspecified" variant.** - See [`section-7-gate.md`](section-7-gate.md) — this is the schema-level - half of keeping §7 an explicit parameter rather than a library default. - -## Non-circular baseline commitment (§5.5) +- **`replaceable` is required non-empty.** It identifies which signer + entries recovery may replace; an empty list would enroll recovery that + can never restore access — rejected at validation, not left as a silent + no-op. In the document it names document-local signer **ids** + (human-reviewable, e.g. `"admin"`); the compiler resolves each to a + `sha256` fingerprint of that signer's actual credential (verifier+key, or + the delegated address) for the wire form the controller stores — not the + id string, which a later document could legitimately reuse for a + different physical key. This is what lets a revoked credential stay + revoked (an old baseline must never restore it) even if a later document + reuses its old id for something else. The controller checks `replaceable` + membership for the caller's *declared* credential list, not the target + document's actual signer diff (it ships no JSON parser) — see + [`controller-governance.md`](controller-governance.md)'s "What the + commitment does, and does not, verify" for that trust boundary. +- **`baseline` is optional, and its presence is what gates + suspected-compromise recovery.** Lost-key recovery needs no predetermined + baseline — it targets the current approved document with designated + credentials replaced. Compromise recovery needs an approved baseline to + restore to, with the same designated credentials replaced. `None` + therefore means "lost-key recovery only," not "recovery is + half-configured." +- **`pending_activity` has no default and no third "unspecified" + variant.** See [`pending-activity-policy.md`](pending-activity-policy.md) + — this is the schema-level half of keeping that choice an explicit, + reviewable parameter rather than a library default. + +## Non-circular baseline commitment `BaselineCommitment { doc_hash: String }` names a **different, earlier** document's canonical hash — never the enclosing document's own hash. There @@ -128,14 +137,14 @@ plain-decimal `u32`s, no `null`) are unchanged; only the set of fields a `PolicyDoc` can carry grew, which every implementation must still agree on byte-for-byte (verified below). -**Testable compatibility condition, and its proof:** the follow-up review's -§5.5 asks whether an additive field can preserve the existing canonical -format — a testable condition, not a blanket claim. It's tested directly: +**Testable compatibility condition, and its proof:** an additive field +should preserve the existing canonical format for documents that don't use +it — a testable condition, not a blanket claim. It's tested directly: `crates/perch-ir/tests/recovery.rs::recovery_absent_documents_hash_exactly_as_before_this_field_existed` asserts the canonical form of a recovery-absent document contains no `"recovery"` substring at all, and every pre-existing fixture (`ci-publish{,-delegated,-threshold}`) and its pinned hash is **unchanged** -by this stage (not regenerated) — their tests in `crates/perch-ir/tests/fixture.rs` +by this change (not regenerated) — their tests in `crates/perch-ir/tests/fixture.rs` and `packages/perch-js/test/parity.test.ts` still pass against the same committed bytes. That is the condition being claimed, made byte-for-byte verifiable rather than asserted. diff --git a/docs/recovery/section-7-gate.md b/docs/recovery/section-7-gate.md deleted file mode 100644 index 1c494f5..0000000 --- a/docs/recovery/section-7-gate.md +++ /dev/null @@ -1,81 +0,0 @@ -# Release gate: pending-activity policy (open, release-blocking) - -**Status: OPEN. This is a release-blocking gate, not a design note.** No -recovery-enabled account should be deployed to production, and no -integration should advertise recovery as safe to rely on, until this gate is -explicitly resolved by the party the follow-up review named to resolve it -(the captain) — not by this codebase, not by a library default, and not by -this PR. - -This mirrors the authoritative decision record's §7 verbatim: *"Decision -status: OPEN, deliberately deferred by the user. No default selected."* That -review also warns explicitly: *"Do not infer an answer from expiry -behavior, the protection profile name, or the fact that this write-up is -finished."* The same warning applies to this codebase shipping: **do not -infer that landing this PR resolves the question.** - -## What is open - -Whether ordinary account-authorized execution, and ordinary policy-mutation -attempts, continue or are blocked while a recovery attempt is pending — and -whether lost-key and suspected-compromise attempts should behave -differently on this axis. Three candidate behaviors were on the table: -freeze, continue, and restrict-selected-operations. None was selected. - -## What this codebase does and does not do about it - -- **The schema forces an explicit, no-default choice per account.** - `RecoveryConfig::pending_activity` (`crates/perch-ir/src/doc.rs`) is a - required field with exactly two variants, `Freeze` and `Continue`, and no - `Default` implementation. `restrict-selected-operations` is deliberately - **not modeled** — adding it needs an explicit capability-boundary design - this stage did not do, so it is out of scope rather than half-built. An - enrolling document must name one of the two modeled choices; there is no - way to enroll recovery without making this choice reviewable in the - document itself. -- **Blocking ordinary *policy-document* mutation while an attempt is - pending is not part of this open question, and is already enforced - unconditionally.** The controller refuses `apply_doc` calls not - authorized by the recovery evidence itself while an attempt is pending — - see [`controller-governance.md`](controller-governance.md) — regardless - of the enrolled `pending_activity` value. This is required for property 9 - (configuration consistency) independent of §7: without it, an admin could - race a pending attempt with a conflicting document change, which §7 never - proposed making optional. `pending_activity` governs a narrower, - genuinely-open question: **ordinary contract calls the account authorizes - that are not policy mutation at all** (moving funds, calling another - contract) — the freeze/continue axis §7 actually names. -- **`Freeze`/`Continue` are recorded and queryable, not fully wired end to - end.** The controller exposes the pending attempt's state and the - enrolled policy so an integration can check it. Actually *enforcing* - `Freeze` against arbitrary non-recovery context rules would require - threading a pending-recovery check through perch-interpreter's - context-rule evaluation for every rule, not only the recovery-authorizing - one — a cross-cutting change to the interpreter's evaluation path that is - explicitly **not implemented in this stage**. Until it is, an account - enrolled with `Freeze` records that intent reviewably, but does not yet - have it mechanically enforced against every other rule on the account. - Shipping that enforcement is itself gated on §7 actually being resolved - (there is no point hardening a mechanism for a policy that might change). - -## What must happen before production reliance - -1. The captain (or whoever the follow-up review names as the decision - owner) resolves §7: freeze, continue, restrict, or some other - precisely-defined rule — including the related, separately-named - question of what happens to a *stale lost-key source snapshot* when - ordinary policy writes are attempted during a pending attempt (block, - invalidate the attempt, or another explicit conflict rule; see the - follow-up review §4.2 and §7). -2. If the resolution requires enforcement beyond what `pending_activity` - already records (i.e. anything beyond "freeze policy mutation," which is - already unconditional), that enforcement is designed and implemented as - its own reviewed change — not inferred from this document or shipped - silently alongside an unrelated change. -3. Only after both of the above should `Protected`-profile, `Freeze`- or - `Continue`-enrolled accounts intended to resist a compromised admin be - treated as production-ready for that specific guarantee. - -Until then: this schema field and the controller's current behavior are -available for experimentation, review, and further design work — not for a -production deployment relying on the pending-activity guarantee to hold. diff --git a/docs/recovery/vk-and-controller-immutability.md b/docs/recovery/vk-and-controller-immutability.md index 726d6ea..d287162 100644 --- a/docs/recovery/vk-and-controller-immutability.md +++ b/docs/recovery/vk-and-controller-immutability.md @@ -1,11 +1,11 @@ # VK, controller, and code immutability review -Per the authoritative decision record §5.4 ("Constructorless verifier and -explicit upgrades") and the captain's instruction to review "code and VK -immutability, controller governance," this document states the invariant -this stage requires, shows the precedent already established elsewhere in -this repo, and states exactly how the new `perch-recovery` crate and any ZK -verifier meet it. +Perch's constructorless-registry model requires verification code and +configuration to stay immutable once deployed, with upgrades happening only +through explicit adoption of a new instance — never in-place mutation. This +document states that invariant, shows the precedent already established +elsewhere in this repo, and states exactly how `perch-recovery` and any ZK +verifier it names meet it. ## The invariant @@ -15,8 +15,8 @@ verifier meet it. > account's own document — never rewriting code, admin state, or verification > configuration at an already-adopted address. -Three consequences follow directly, and are treated as hard requirements -for anything this stage ships: +Three consequences follow directly, and are treated as hard requirements for +`perch-recovery` and anything it names: 1. **No constructor that sets mutable configuration.** If a contract's verification behavior (a VK, a circuit identity, a policy the controller @@ -25,11 +25,10 @@ for anything this stage ships: deployer, the artifact is not immutable, no matter how it was deployed. 2. **No admin/owner entry point at all.** An "admin-gated upgrade" is not the same guarantee as immutability — it's a promise that the admin key - won't be misused, which is exactly the kind of trust the "no mutable - shared controller... may silently bypass commitment" language in §5.4 - rules out. The bar is that there is **nothing to misuse**: no pause - switch, no fee/parameter setter, no logic branch keyed by any mutable - flag. + won't be misused, and a mutable shared controller with such a key could + silently bypass whatever commitment it's supposed to enforce. The bar is + that there is **nothing to misuse**: no pause switch, no fee/parameter + setter, no logic branch keyed by any mutable flag. 3. **Identity is content, not address history.** Deploying at a content-addressed address (`deployer(stateless_registry_id, sha256(wasm))`, as `perch-doc-compiler` and `perch-interpreter` already @@ -42,7 +41,7 @@ for anything this stage ships: ## Precedent already in this repo `perch-doc-compiler` and `perch-interpreter` already meet this bar today, -and are the model this stage follows rather than invents: +and are the model `perch-recovery` follows rather than invents: ```rust #[cfg(feature = "contract")] @@ -68,7 +67,7 @@ from a build-time-pinned WASM hash — see makes a schema change require a new deployed instance rather than an in-place change. -## What this stage adds, and how each piece meets the bar +## How `perch-recovery` and a ZK verifier meet the bar - **The recovery controller (`perch-recovery`).** No constructor beyond whatever the Soroban toolchain requires for deployment plumbing; no @@ -80,21 +79,21 @@ in-place change. a controller-wide admin key. See [`controller-governance.md`](controller-governance.md) for the exact entry points and their authorization requirements. -- **A ZK verifier, if a deployment enrolls a ZK-involving mode.** The bar - from §5.4 is explicit: *"Embedding the VK and proof-format identity in the - artifact is a straightforward candidate... Merely removing a constructor - while retaining mutable verification configuration is insufficient."* - Concretely, any verifier a `ZkVerifierConfig.verifier` names must: +- **A ZK verifier, if a deployment enrolls a ZK-involving mode.** Embedding + the VK and proof-format identity in the artifact is the straightforward + way to meet the bar above — merely removing a constructor while retaining + mutable verification configuration would not be enough. Concretely, any + verifier a `ZkVerifierConfig.verifier` names must: - embed its verification key as a compiled-in constant (e.g. `include_bytes!` at build time), never a storage value a constructor or any other entry point writes; - expose no entry point that changes which VK or circuit it accepts; - commit to its circuit/proof-format identity in a form the account's own - document also carries (`ZkVerifierConfig.circuit_id`, §5.4's "defense in - depth" binding) — so a verifier address confused with another still - fails the circuit-identity check, not just the address check. + document also carries (`ZkVerifierConfig.circuit_id`, a defense-in-depth + binding) — so a verifier address confused with another still fails the + circuit-identity check, not just the address check. - This stage does not ship a production circuit or its verifier (see + This crate does not ship a production circuit or its verifier (see [`controller-governance.md`](controller-governance.md)'s "ZK adapter scope" section for why that's a deliberate, documented boundary, not an oversight) — but the controller's ZK adapter interface is written against From cedd5d0a87739e2350bf75d03fb38d0e06e75422 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 14 Sep 2026 22:19:50 -0400 Subject: [PATCH 6/7] refactor(recovery): convert 0-or-1 Vec fields to Option where the SDK allows it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every field across the compiler and recovery wire types documented as holding "zero or one" entries in a `Vec` (a pattern used to emulate `Option` around a Soroban SDK limitation). Empirically confirmed the exact limitation first (a throwaway probe test, not committed): `#[contracttype]`'s derive macro has no `ScVal` conversion for `Option` where `T` is itself a custom `#[contracttype]` struct or enum ("the trait bound `ScVal: TryFrom<&Option>` is not satisfied") — but `Option` for a host-builtin T (`BytesN<32>`, `Address`, `Bytes`, `u32`, ...) works fine, verified via a full round-trip through a generated `#[contractclient]`. Converted (element type is a host builtin, so `Option` compiles and the invariant is now type-enforced instead of documented-and-hoped): - `CompiledRecoveryConfig::baseline`: `Vec>` -> `Option>` - `CompiledZkVerifierConfig::pool`: `Vec
` -> `Option
` (and `ZkVerifierInterface::verify_proof`'s `pool` parameter to match) - `ReconfigureEvidence::zk_nullifier`/`zk_proof`: `Vec>`/`Vec` -> `Option>`/`Option` (kept as two parallel `Option`s, not bundled into one `Option<(nullifier, proof)>` — a tuple or wrapper struct would hit the same custom-type limitation) - `Attempt::nullifier`: `Vec>` -> `Option>` Left as `Vec` with the exact reason now stated at each field (element type is itself a `#[contracttype]` struct, so `Option` doesn't compile): - `CompiledRule::install: Vec` - `CompiledRule::cap: Vec` - `CompiledDoc::recovery: Vec` `CompiledRecoveryConfig::replaceable` was checked and is genuinely multi-valued (a set of many credential fingerprints), not a 0-or-1 field — left as `Vec` with no change. This is a wire-shape-only change: `perch-ir`'s canonical-JSON layer (`crates/perch-ir/`, `testdata/*.canonical.json`, `*.doc-hash`) and `packages/perch-js/` are untouched (confirmed by empty diffs) and continue to pass unmodified, because `doc_hash` is computed from the canonical JSON document before compilation — it has no dependency on how the *compiled* wire types are laid out. Updated call sites in `perch-recovery::contract` and the `no_recovery_evidence` test helper accordingly; test snapshots regenerated (Vec `{vec: []}`/`{vec: [x]}` XDR shapes become `void`/`x` directly). Full workspace test/fmt/clippy -D warnings clean, wasm builds verified via `stellar contract build` for perch-recovery/perch-doc-compiler/ perch-smart-account/perch-account, perch-js suite clean (48 tests). --- ..._once_its_evidence_deadline_elapses.1.json | 24 ++---- ...ttempts_nullifier_is_never_released.1.json | 20 ++--- ...lls_rules_and_stores_canonical_hash.1.json | 8 +- ...alls_the_cap_beside_the_interpreter.1.json | 8 +- ...t_that_is_not_the_enrolled_baseline.1.json | 18 +---- ...ardian_quorum_alone_does_not_cancel.1.json | 24 ++---- ...tion_zk_proof_alone_does_not_cancel.1.json | 24 ++---- .../completion_cannot_be_replayed.1.json | 20 ++--- ...refused_before_the_timelock_elapses.1.json | 20 ++--- ...etion_refused_below_guardian_quorum.1.json | 18 ++--- ...fused_for_the_wrong_target_document.1.json | 20 ++--- ...ion_refused_with_no_evidence_at_all.1.json | 16 +--- ...fingerprint_is_hex_case_insensitive.1.json | 20 ++--- ...ate_domain_from_initiation_approval.1.json | 22 ++---- ...used_once_the_attempt_has_completed.1.json | 18 ++--- ...es_and_installs_the_target_document.1.json | 20 ++--- ...ever_promotes_without_being_reached.1.json | 22 ++---- ...ian_evidence_admin_alone_is_refused.1.json | 12 +-- ...reapply_replaces_the_whole_rule_set.1.json | 16 +--- ...used_once_the_attempt_has_completed.1.json | 22 ++---- ...cancellation_requires_a_valid_proof.1.json | 20 ++--- ...ediately_not_deferred_to_completion.1.json | 22 ++---- crates/integration-tests/tests/recovery.rs | 3 +- crates/perch-doc-compiler/src/lib.rs | 75 +++++++++++-------- crates/perch-recovery/src/contract.rs | 28 ++++--- crates/perch-recovery/src/lib.rs | 13 +++- crates/perch-recovery/src/types.rs | 10 ++- crates/perch-recovery/src/zk.rs | 9 ++- crates/perch-testkit/src/fixture.rs | 4 +- 29 files changed, 192 insertions(+), 364 deletions(-) diff --git a/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json b/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json index 0b7986f..8afe9e7 100644 --- a/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json +++ b/crates/integration-tests/test_snapshots/a_collecting_evidence_attempt_stops_blocking_apply_doc_once_its_evidence_deadline_elapses.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -91,17 +87,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -665,9 +657,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -741,9 +731,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json index 16f3a07..317d8df 100644 --- a/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json +++ b/crates/integration-tests/test_snapshots/a_completed_attempts_nullifier_is_never_released.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -612,9 +608,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -688,9 +682,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -747,9 +739,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json b/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json index de18a82..86e567d 100644 --- a/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json +++ b/crates/integration-tests/test_snapshots/apply_doc_installs_rules_and_stores_canonical_hash.1.json @@ -38,17 +38,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } diff --git a/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json b/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json index 931a572..329be8a 100644 --- a/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json +++ b/crates/integration-tests/test_snapshots/apply_doc_installs_the_cap_beside_the_interpreter.1.json @@ -38,17 +38,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } diff --git a/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json b/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json index 7fd91bd..433f6f4 100644 --- a/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json +++ b/crates/integration-tests/test_snapshots/begin_compromise_attempt_rejects_a_target_that_is_not_the_enrolled_baseline.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -597,9 +593,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -674,11 +668,7 @@ "symbol": "baseline" }, "val": { - "vec": [ - { - "bytes": "0303030303030303030303030303030303030303030303030303030303030303" - } - ] + "bytes": "0303030303030303030303030303030303030303030303030303030303030303" } }, { diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json index a6f744c..dd68ac3 100644 --- a/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json +++ b/crates/integration-tests/test_snapshots/combined_cancellation_guardian_quorum_alone_does_not_cancel.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -73,7 +69,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" + "bytes": "7fd7c6ab92eafab7b71de4b7f94efc3469582a2474e34e1ee58b83093ac3e915" } ] } @@ -92,7 +88,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" + "bytes": "7fd7c6ab92eafab7b71de4b7f94efc3469582a2474e34e1ee58b83093ac3e915" } ] } @@ -636,9 +632,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -780,9 +774,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -866,9 +858,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json index 279e9a2..fcabc29 100644 --- a/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json +++ b/crates/integration-tests/test_snapshots/combined_cancellation_zk_proof_alone_does_not_cancel.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -75,7 +71,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" + "bytes": "7fd7c6ab92eafab7b71de4b7f94efc3469582a2474e34e1ee58b83093ac3e915" } ] } @@ -94,7 +90,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "7861f13b019e85e312d1a6e1713a81cbeeed95f1950f2ff79dcf90042981231f" + "bytes": "7fd7c6ab92eafab7b71de4b7f94efc3469582a2474e34e1ee58b83093ac3e915" } ] } @@ -636,9 +632,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -780,9 +774,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -866,9 +858,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json index 4263047..082bf1f 100644 --- a/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json +++ b/crates/integration-tests/test_snapshots/completion_cannot_be_replayed.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -103,7 +99,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -653,9 +649,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -729,9 +723,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json index 00bd237..c6a9287 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_before_the_timelock_elapses.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -103,7 +99,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -652,9 +648,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -728,9 +722,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json index 00110b7..b62380e 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_below_guardian_quorum.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -630,9 +626,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -706,9 +700,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json index 3c21e6b..c3a8ee7 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_for_the_wrong_target_document.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -103,7 +99,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -652,9 +648,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -728,9 +722,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json index 3c4e3f4..4d6ca2a 100644 --- a/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json +++ b/crates/integration-tests/test_snapshots/completion_refused_with_no_evidence_at_all.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -606,9 +602,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -682,9 +676,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json b/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json index c54c0c6..5b96e6e 100644 --- a/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json +++ b/crates/integration-tests/test_snapshots/credential_fingerprint_is_hex_case_insensitive.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -87,17 +83,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -601,9 +593,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json index c2ef6ea..a492aaf 100644 --- a/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json +++ b/crates/integration-tests/test_snapshots/guardian_cancel_is_a_separate_domain_from_initiation_approval.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -72,7 +68,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -91,7 +87,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "618e4ee371df04b636c4f597f9854fd7ecc63d67ea963f3cc49ff2f4ba6ee3c6" + "bytes": "656069443256f83f94f0d7af1bbda4813588eac5e6eab6444c8fe32ef3c06430" } ] } @@ -111,7 +107,7 @@ "function_name": "submit_guardian_cancel", "args": [ { - "bytes": "618e4ee371df04b636c4f597f9854fd7ecc63d67ea963f3cc49ff2f4ba6ee3c6" + "bytes": "656069443256f83f94f0d7af1bbda4813588eac5e6eab6444c8fe32ef3c06430" } ] } @@ -654,9 +650,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -798,9 +792,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json index 17821ed..0ac728e 100644 --- a/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json +++ b/crates/integration-tests/test_snapshots/guardian_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "5c15ca812c2379c45352526f6146e419a6102f13a4bfebae465df84d2b3e1ef5" + "bytes": "eb2d5a8f4f8ce3228829bbeea2fdc6f9c4b834c950648d583be8c82a22a971c0" } ] } @@ -633,9 +629,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -709,9 +703,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json index 282f08a..e150f66 100644 --- a/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json +++ b/crates/integration-tests/test_snapshots/guardian_only_lost_key_recovery_completes_and_installs_the_target_document.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -84,7 +80,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -103,7 +99,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "06ab197ba264900eb4fbdbf1f282d65e42122e86df832e04dbfa40d8bbedeaee" + "bytes": "6e84739e72fd5af2aeb72ad49a8caba6fe505544f045c3552271654901f3f1c6" } ] } @@ -655,9 +651,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -731,9 +725,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json index 51e6b1d..184752f 100644 --- a/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json +++ b/crates/integration-tests/test_snapshots/guardian_quorum_alone_never_promotes_without_being_reached.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -72,7 +68,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" + "bytes": "3904d76ce3487706b566befa8525d7e7851a4075a3ae703054dc80a227f3ec2f" } ] } @@ -91,7 +87,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" + "bytes": "3904d76ce3487706b566befa8525d7e7851a4075a3ae703054dc80a227f3ec2f" } ] } @@ -111,7 +107,7 @@ "function_name": "submit_guardian_approval", "args": [ { - "bytes": "6d4bc1ccd20fd5bd391fd56384c1dbda70e556d2a59967b4f20e875b7d3d4ca8" + "bytes": "3904d76ce3487706b566befa8525d7e7851a4075a3ae703054dc80a227f3ec2f" } ] } @@ -663,9 +659,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -739,9 +733,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json index 83d6385..4137674 100644 --- a/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json +++ b/crates/integration-tests/test_snapshots/protected_reconfigure_requires_guardian_evidence_admin_alone_is_refused.1.json @@ -39,17 +39,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -534,9 +530,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json b/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json index 6932884..901cf16 100644 --- a/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json +++ b/crates/integration-tests/test_snapshots/reapply_replaces_the_whole_rule_set.1.json @@ -38,17 +38,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -85,17 +81,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } diff --git a/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json index 65e232f..43c2c0a 100644 --- a/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json +++ b/crates/integration-tests/test_snapshots/zk_cancellation_is_refused_once_the_attempt_has_completed.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -613,11 +609,7 @@ "symbol": "nullifier" }, "val": { - "vec": [ - { - "bytes": "0606060606060606060606060606060606060606060606060606060606060606" - } - ] + "bytes": "0606060606060606060606060606060606060606060606060606060606060606" } }, { @@ -692,9 +684,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -751,9 +741,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json index 980a6b7..0d5a402 100644 --- a/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json +++ b/crates/integration-tests/test_snapshots/zk_only_cancellation_requires_a_valid_proof.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -599,9 +595,7 @@ "key": { "symbol": "nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -702,9 +696,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -761,9 +753,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json b/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json index e13c701..f966e9c 100644 --- a/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json +++ b/crates/integration-tests/test_snapshots/zk_proof_nullifier_is_reserved_immediately_not_deferred_to_completion.1.json @@ -40,17 +40,13 @@ "key": { "symbol": "zk_nullifier" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { "symbol": "zk_proof" }, - "val": { - "vec": [] - } + "val": "void" } ] } @@ -599,11 +595,7 @@ "symbol": "nullifier" }, "val": { - "vec": [ - { - "bytes": "0909090909090909090909090909090909090909090909090909090909090909" - } - ] + "bytes": "0909090909090909090909090909090909090909090909090909090909090909" } }, { @@ -678,9 +670,7 @@ "key": { "symbol": "baseline" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { @@ -737,9 +727,7 @@ "key": { "symbol": "pool" }, - "val": { - "vec": [] - } + "val": "void" }, { "key": { diff --git a/crates/integration-tests/tests/recovery.rs b/crates/integration-tests/tests/recovery.rs index a5cbb38..a4f2ed1 100644 --- a/crates/integration-tests/tests/recovery.rs +++ b/crates/integration-tests/tests/recovery.rs @@ -22,7 +22,6 @@ use soroban_sdk::auth::{Context, ContractContext}; use soroban_sdk::testutils::{Address as _, Ledger}; use soroban_sdk::{ contract, contractimpl, crypto::Hash, vec, Address, Bytes, BytesN, Env, IntoVal, Map, Symbol, - Vec, }; use stellar_accounts::smart_account::{do_check_auth, AuthPayload}; @@ -102,7 +101,7 @@ impl MockZkVerifier { _statement: BytesN<32>, _nullifier: BytesN<32>, proof: Bytes, - _pool: Vec
, + _pool: Option
, ) -> bool { !proof.is_empty() } diff --git a/crates/perch-doc-compiler/src/lib.rs b/crates/perch-doc-compiler/src/lib.rs index 23a6414..9452f92 100644 --- a/crates/perch-doc-compiler/src/lib.rs +++ b/crates/perch-doc-compiler/src/lib.rs @@ -66,16 +66,23 @@ pub struct CompiledRule { pub scope: RuleScope, pub signers: Vec, pub valid_until: Option, - /// Zero or one entries — a `Vec` rather than `Option` because - /// `Option` cannot cross the ScVal boundary that testutils - /// clients use. Empty ⇒ policy-free rule. + /// Zero or one entries — a `Vec` rather than `Option`-style + /// `Option` because `InstallParams` is itself a `#[contracttype]` struct, + /// and `#[contracttype]`'s derive macro has no `ScVal`/spec conversion + /// for `Option` where `T` is a custom struct or enum (confirmed + /// directly: it fails to compile with "the trait bound `ScVal: + /// TryFrom<&Option>` is not satisfied" — `Option` on + /// `valid_until` above works because `u32` is a host-builtin type with + /// its own direct `ScVal` conversion, not because `Option` itself is the + /// problem). Empty ⇒ policy-free rule. pub install: Vec, - /// Zero or one entries (a `Vec` for the same ScVal reason as `install`). - /// Present ⇒ also attach OZ `spending_limit` with these params — the - /// cumulative cap the stateless interpreter cannot express. The applier - /// resolves the policy's content-addressed address and keys it into the - /// rule's policy map beside the interpreter; the tracked token is the rule's - /// `Contract` scope (validation pins `token == scope`). + /// Zero or one entries (a `Vec` for the same reason as `install` — + /// `CompiledCap` is itself a `#[contracttype]` struct). Present ⇒ also + /// attach OZ `spending_limit` with these params — the cumulative cap the + /// stateless interpreter cannot express. The applier resolves the + /// policy's content-addressed address and keys it into the rule's policy + /// map beside the interpreter; the tracked token is the rule's `Contract` + /// scope (validation pins `token == scope`). pub cap: Vec, } @@ -100,14 +107,16 @@ pub struct CompiledDoc { /// minified twin compile to the same hash. pub doc_hash: BytesN<32>, pub rules: Vec, - /// Zero or one entries (a `Vec` for the same ScVal reason as - /// [`CompiledRule::install`]). Present ⇒ the document enrolls account - /// recovery — the applier is expected to sync this configuration to the - /// adopted recovery-controller instance named in it. See - /// `docs/recovery/` for the full design; wire-compat notes live there too - /// (this is a new field on an existing constructorless, immutable - /// deployable — a compiler build carrying it is a new instance, not an - /// in-place change to any already-deployed one). + /// Zero or one entries — a `Vec` because `CompiledRecoveryConfig` is + /// itself a `#[contracttype]` struct (see [`CompiledRule::install`]'s + /// doc comment for why `Option` doesn't compile + /// here). Present ⇒ the document enrolls account recovery — the applier + /// is expected to sync this configuration to the adopted + /// recovery-controller instance named in it. See `docs/recovery/` for + /// the full design; wire-compat notes live there too (this is a new + /// field on an existing constructorless, immutable deployable — a + /// compiler build carrying it is a new instance, not an in-place change + /// to any already-deployed one). pub recovery: Vec, } @@ -119,10 +128,13 @@ pub struct CompiledRecoveryConfig { pub profile: RecoveryProfile, pub mode: CompiledRecoveryMode, pub controller: Address, - /// Zero or one entries (a `Vec` for the same ScVal reason as - /// [`CompiledRule::install`]). Present ⇒ suspected-compromise recovery is - /// enrolled, restoring the document this hash names. - pub baseline: Vec>, + /// `Some` ⇒ suspected-compromise recovery is enrolled, restoring the + /// document this hash names. A plain `Option`, unlike + /// [`CompiledRule::install`]/`cap`/[`CompiledDoc::recovery`] above: + /// `BytesN<32>` is a host-builtin type (its own direct `ScVal` + /// conversion), not a `#[contracttype]` struct, so the derive-macro + /// limitation those fields work around doesn't apply here. + pub baseline: Option>, /// Fingerprint of each replaceable signer's *physical credential* /// (`sha256` of a tagged encoding of its `SignerMethod` — verifier+key for /// `external`, the address for `delegated`), resolved from @@ -178,9 +190,11 @@ pub struct CompiledZkVerifierConfig { pub verifier: Address, /// Decoded from the document's hex `circuit-id`. pub circuit_id: Bytes, - /// Zero or one entries (a `Vec` for the same ScVal reason as - /// [`CompiledRule::install`]). - pub pool: Vec
, + /// A membership-pool contract's address, for ZK schemes that prove + /// knowledge of one fixed secret against a set the pool contract tracks; + /// `None` for schemes with no pool. `Address` is a host-builtin type, so + /// (unlike [`CompiledRule::install`]/`cap`) a plain `Option` works here. + pub pool: Option
, } /// Cross-contract client, generated independently of the deployable so @@ -287,10 +301,10 @@ fn to_compiled_recovery( to_compiled_zk_verifier_config(e, z)?, ), }; - let mut baseline: Vec> = Vec::new(e); - if let Some(b) = &r.baseline { - baseline.push_back(hex_bytes_32(e, &b.doc_hash)?); - } + let baseline = match &r.baseline { + Some(b) => Some(hex_bytes_32(e, &b.doc_hash)?), + None => None, + }; // Precondition (validate(doc).is_ok(), guaranteed by the one caller): // every `replaceable` id references a declared signer // (`UnknownRecoveryReplaceableRef` would already have failed validation), @@ -367,10 +381,7 @@ fn to_compiled_zk_verifier_config( e: &Env, z: &perch_ir::ZkVerifierConfig, ) -> Result { - let mut pool: Vec
= Vec::new(e); - if let Some(p) = &z.pool { - pool.push_back(Address::from_str(e, p)); - } + let pool = z.pool.as_ref().map(|p| Address::from_str(e, p)); Ok(CompiledZkVerifierConfig { verifier: Address::from_str(e, &z.verifier), circuit_id: hex_bytes(e, &z.circuit_id)?, diff --git a/crates/perch-recovery/src/contract.rs b/crates/perch-recovery/src/contract.rs index 91aa9ac..9657170 100644 --- a/crates/perch-recovery/src/contract.rs +++ b/crates/perch-recovery/src/contract.rs @@ -197,9 +197,9 @@ fn complete(e: &Env, context: &Context, smart_account: &Address) { } RecoveryStorage::set_revoked(e, smart_account, &revoked); RecoveryStorage::extend_revoked_ttl(e, smart_account, TTL_THRESHOLD, max_ttl(e)); - if let Some(n) = attempt.nullifier.first() { - RecoveryStorage::set_nullifier(e, &n, &true); - RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, max_ttl(e)); + if let Some(n) = &attempt.nullifier { + RecoveryStorage::set_nullifier(e, n, &true); + RecoveryStorage::extend_nullifier_ttl(e, n, TTL_THRESHOLD, max_ttl(e)); } RecoveryCompleted { account: smart_account.clone(), @@ -344,7 +344,7 @@ impl PerchRecovery { replaced_credentials: Vec>, ) -> Result { let config = require_config(e, &account)?; - let Some(baseline) = config.baseline.first() else { + let Some(baseline) = config.baseline else { return Err(RecoveryError::NoBaselineEnrolled); }; if baseline != target_doc_hash { @@ -450,9 +450,7 @@ impl PerchRecovery { RecoveryStorage::set_nullifier(e, &nullifier, &true); RecoveryStorage::extend_nullifier_ttl(e, &nullifier, TTL_THRESHOLD, max_ttl(e)); attempt.zk_verified = true; - let mut nul = Vec::new(e); - nul.push_back(nullifier); - attempt.nullifier = nul; + attempt.nullifier = Some(nullifier); maybe_promote(e, &account, &config, &mut attempt)?; RecoveryStorage::set_attempt(e, &account, &attempt); RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, max_ttl(e)); @@ -614,8 +612,8 @@ impl PerchRecovery { } if let Some(attempt) = RecoveryStorage::get_attempt(e, &account) { RecoveryStorage::extend_attempt_ttl(e, &account, TTL_THRESHOLD, ttl); - if let Some(n) = attempt.nullifier.first() { - RecoveryStorage::extend_nullifier_ttl(e, &n, TTL_THRESHOLD, ttl); + if let Some(n) = &attempt.nullifier { + RecoveryStorage::extend_nullifier_ttl(e, n, TTL_THRESHOLD, ttl); } } if RecoveryStorage::has_revoked(e, &account) { @@ -769,8 +767,8 @@ fn begin_attempt( // itself already extends the nullifier's own TTL as the durable // record of that. if existing.state != AttemptState::Completed { - if let Some(n) = existing.nullifier.first() { - RecoveryStorage::set_nullifier(e, &n, &false); + if let Some(n) = &existing.nullifier { + RecoveryStorage::set_nullifier(e, n, &false); } } } @@ -801,7 +799,7 @@ fn begin_attempt( expires_at: 0, guardian_approvals: Vec::new(e), zk_verified: false, - nullifier: Vec::new(e), + nullifier: None, state: AttemptState::CollectingEvidence, }; RecoveryStorage::set_attempt(e, &account, &attempt); @@ -820,8 +818,8 @@ fn cancel_attempt(e: &Env, account: &Address, attempt: &mut Attempt) -> Result<( // silently reset to 0 via TTL expiry — extend on every write. RecoveryStorage::extend_cancels_used_ttl(e, account, TTL_THRESHOLD, max_ttl(e)); attempt.state = AttemptState::Cancelled; - if let Some(n) = attempt.nullifier.first() { - RecoveryStorage::set_nullifier(e, &n, &false); + if let Some(n) = &attempt.nullifier { + RecoveryStorage::set_nullifier(e, n, &false); } RecoveryStorage::set_attempt(e, account, attempt); RecoveryCancelled { @@ -861,7 +859,7 @@ fn require_zk_evidence( evidence: &ReconfigureEvidence, digest: &BytesN<32>, ) -> Result<(), RecoveryError> { - let (Some(nullifier), Some(proof)) = (evidence.zk_nullifier.first(), evidence.zk_proof.first()) + let (Some(nullifier), Some(proof)) = (evidence.zk_nullifier.clone(), evidence.zk_proof.clone()) else { return Err(RecoveryError::ReconfigureEvidenceRequired); }; diff --git a/crates/perch-recovery/src/lib.rs b/crates/perch-recovery/src/lib.rs index df006bd..5b43261 100644 --- a/crates/perch-recovery/src/lib.rs +++ b/crates/perch-recovery/src/lib.rs @@ -86,9 +86,16 @@ pub struct ReconfigureEvidence { /// which must independently authorize this exact call (the host rejects /// the whole invocation if any named address didn't actually sign). pub guardians: Vec
, - /// Zero or one `(nullifier, proof)` pair, for a ZK-capable enrolled mode. - pub zk_nullifier: Vec>, - pub zk_proof: Vec, + /// Zero or one `(nullifier, proof)` pair, for a ZK-capable enrolled + /// mode — two parallel `Option`s rather than one + /// `Option<(BytesN<32>, Bytes)>` (or a dedicated struct) because a tuple + /// isn't a `#[contracttype]`-representable field type and a wrapper + /// struct would hit the same `Option` limitation + /// `CompiledRule::install` documents — `BytesN<32>`/`Bytes` are + /// host-builtin types, so a plain `Option` works for each individually. + /// `require_zk_evidence` requires both present or both absent. + pub zk_nullifier: Option>, + pub zk_proof: Option, } /// Cross-contract client, generated independently of the deployable (see diff --git a/crates/perch-recovery/src/types.rs b/crates/perch-recovery/src/types.rs index c562c28..78c53f9 100644 --- a/crates/perch-recovery/src/types.rs +++ b/crates/perch-recovery/src/types.rs @@ -91,9 +91,11 @@ pub struct Attempt { pub guardian_approvals: Vec
, /// Whether a valid initiation proof has been submitted for this attempt. pub zk_verified: bool, - /// Nullifier consumed by this attempt's ZK evidence, if any (zero or one - /// entry) — released back to unspent if the attempt is replaced before - /// promotion, spent permanently on completion. - pub nullifier: Vec>, + /// Nullifier consumed by this attempt's ZK evidence, if any — released + /// back to unspent if the attempt is replaced before promotion, spent + /// permanently on completion. `BytesN<32>` is a host-builtin type, so + /// (unlike a `Vec`-emulated `Option` over a custom `#[contracttype]`, + /// e.g. `CompiledRule::install`) a plain `Option` works here. + pub nullifier: Option>, pub state: AttemptState, } diff --git a/crates/perch-recovery/src/zk.rs b/crates/perch-recovery/src/zk.rs index c2f1a37..ac403c7 100644 --- a/crates/perch-recovery/src/zk.rs +++ b/crates/perch-recovery/src/zk.rs @@ -11,7 +11,7 @@ use crate::types::Action; use soroban_sdk::xdr::ToXdr; -use soroban_sdk::{Address, Bytes, BytesN, Env, Vec}; +use soroban_sdk::{Address, Bytes, BytesN, Env}; /// A domain-separation tag, so a statement can never be confused with a hash /// computed for an unrelated purpose (`doc_hash`, `config_hash`, ...) even if @@ -73,8 +73,9 @@ pub fn statement( /// /// `nullifier` is the prover-revealed nullifier the circuit derives from its /// secret; the controller tracks spent nullifiers itself (globally, across -/// accounts — see `storage.rs`), so a verifier need not. `pool`, present only -/// for schemes that prove membership of a secret in a set, is opaque to the +/// accounts — see `storage.rs`), so a verifier need not. `pool` — a +/// membership-pool contract's address, present only for schemes that prove +/// knowledge of a secret against a set the pool tracks — is opaque to the /// controller: it is passed through unchanged from the enrolled /// `ZkVerifierConfig.pool`, and interpreting it is entirely the verifier's /// concern. @@ -86,7 +87,7 @@ pub trait ZkVerifierInterface { statement: BytesN<32>, nullifier: BytesN<32>, proof: Bytes, - pool: Vec
, + pool: Option
, ) -> bool; } diff --git a/crates/perch-testkit/src/fixture.rs b/crates/perch-testkit/src/fixture.rs index 3d27078..bdd9dcb 100644 --- a/crates/perch-testkit/src/fixture.rs +++ b/crates/perch-testkit/src/fixture.rs @@ -38,8 +38,8 @@ pub const CI_PUBLISH_DOC_HASH: &str = pub fn no_recovery_evidence(env: &Env) -> perch_recovery::ReconfigureEvidence { perch_recovery::ReconfigureEvidence { guardians: Vec::new(env), - zk_nullifier: Vec::new(env), - zk_proof: Vec::new(env), + zk_nullifier: None, + zk_proof: None, } } From 800a1604a29b6448f233cda75770da0a73aab587 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 16 Sep 2026 14:24:14 -0400 Subject: [PATCH 7/7] fix(release): auto-sync intra-workspace pins after a contract bump; fix stale doc path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New review comments on PR 83: - release.yml's release-pr job bumped a contract's own Cargo.toml version but never touched any *dependent* crate's `{ version = "...", path = "..." }` pin on it — including tracked contracts (perch-recovery pins perch-doc-compiler) and dev-only consumers (perch-testkit, integration-tests pin perch-recovery). A 0.x minor bump is breaking under Cargo's caret rules, so a stale pin fails `cargo metadata` workspace-wide the next time anything touches the dependent crate — exactly what happened once already (PR #79, fixed by hand at the time). Added a sync pass after the per-contract bump loop: for every crate actually bumped this run, grep every crates/*/Cargo.toml for a pin naming it and rewrite that pin's version in place. Verified the exact sed/grep logic against a real Ubuntu container (matching the runs-on: ubuntu-latest / GNU sed the job actually executes under — a first pass tested against this machine's BSD sed and produced a false negative, since BSD sed's `-i` argument handling differs from GNU's) with a stubbed git-cliff forcing a 0.x minor bump on two contracts; confirmed every dependent pin (tracked and dev-only alike) updates correctly while preserving surrounding TOML formatting. - A `publish-plan` comment still pointed at the pre-rename `docs/recovery/section-7-gate.md`; corrected to `docs/recovery/pending-activity-policy.md`. Updated AGENTS.md's release-pipeline sharp-edge entry to describe the new automated behavior instead of the old "grep and bump by hand" instruction, which this change makes obsolete for anything going through `release-pr`. actionlint clean; full workspace cargo test/fmt/clippy -D warnings clean (unaffected — workflow/doc-only change). --- .github/workflows/release.yml | 26 +++++++++++++++++++++++--- AGENTS.md | 30 +++++++++++++++--------------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a50aeac..37276ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -249,6 +249,7 @@ jobs: esac; } CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry perch-recovery" summary="" + bumped="" for c in $CONTRACTS; do inc=() for d in $(paths_for "$c"); do inc+=(--include-path "crates/$d/**"); done @@ -266,7 +267,25 @@ jobs: git cliff --config cliff.toml --tag-pattern "$c-v.*" "${inc[@]}" --bump -o "crates/$c/CHANGELOG.md" 2>/dev/null summary="${summary}- \`$c\`: $cur -> $next_ver"$'\n' echo "bump $c: $cur -> $next_ver" + bumped="${bumped}$c=$next_ver"$'\n' done + # Sync every intra-workspace path-dependency pin to the version its + # target crate just moved to. Without this, a 0.x MINOR bump here + # (breaking, under Cargo's caret rules) leaves a dependent's pin + # stale and `cargo metadata` fails workspace-wide the next time + # anything touches that dependent — see AGENTS.md's version-pin + # sharp edge (this used to be a manual "grep and bump every match" + # step, which is exactly how that failure mode happened before). + if [ -n "$bumped" ]; then + while IFS='=' read -r name ver; do + [ -n "$name" ] || continue + for f in crates/*/Cargo.toml; do + grep -qE "^${name} = \{ version = \"[^\"]*\", path = " "$f" || continue + sed -i -E "s|^(${name} = \{ version = \")[^\"]*(\", path = )|\1${ver}\2|" "$f" + echo "synced pin: $f -> $name $ver" + done + done <<<"$bumped" + fi # The npm packages ride the same convention: tag-pattern -v*, # scoped to packages/. Bump via `npm version` so # package-lock.json stays in sync with the manifest. @@ -625,9 +644,10 @@ jobs: # perch-recovery is deliberately NOT in this allow-list yet: it is # tagged/versioned (CONTRACTS above) so its intra-workspace pins stay # tracked, but publishing a recovery controller on-chain is a - # separate, deliberate decision (see docs/recovery/section-7-gate.md) - # — add it here explicitly when that decision is made, not as a side - # effect of adding the crate. + # separate, deliberate decision (see + # docs/recovery/pending-activity-policy.md) — add it here explicitly + # when that decision is made, not as a side effect of adding the + # crate. ALLOW='["perch-interpreter","perch-doc-compiler","perch-ed25519-verifier"]' if [ -n "$DISPATCH_TAG" ]; then # Strict allow-list regex, not a prefix glob: the tag and the package diff --git a/AGENTS.md b/AGENTS.md index 8da26dc..2e9edcb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,24 +4,24 @@ This file is the project's committed home for project-intrinsic agent knowledge: - Add durable project-specific notes here as they are discovered through real work. -## Release pipeline sharp edge: dev-only crates aren't in a contract's version-bump scope +## Release pipeline: intra-workspace pins are auto-synced after each bump `.github/workflows/release.yml`'s `release-pr` job (git-cliff) computes each contract's next version from commits touching a fixed `paths_for()` path list -per contract (see the job for the current lists). Crates that are **only** -`[dev-dependencies]` consumers of a contract — `crates/integration-tests`, -`crates/perch-testkit` — are not in any contract's scope, so a commit that -only touches them never triggers a version bump there, and their -intra-workspace `path` dependency version pins (e.g. `perch-account = { -version = "0.1.1", path = "..." }`) can silently go stale when a contract -bumps a 0.x **minor** version (Cargo's caret rules treat 0.x minor bumps as -breaking). A stale pin fails `cargo metadata` for the **whole workspace**, -which only surfaces when `release.yml`'s `constructorless-build` job runs for -an unrelated contract — see PR #79 for a case where this silently blocked -`perch-doc-compiler-v0.2.0`'s publish for a day. When bumping any contract's -version, grep for its name across `crates/*/Cargo.toml` `{ version = "...", -path = "..." }` pins and bump every match, not just the crates in that -contract's `paths_for()` scope. +per contract (see the job for the current lists), then — after the bump +loop — greps **every** `crates/*/Cargo.toml` (not just the crates in scope +for a given contract, and not just other tracked contracts; dev-only +consumers like `crates/integration-tests`/`crates/perch-testkit` are +included) for a `{ version = "...", path = "..." }` pin naming a +just-bumped crate, and rewrites that pin's version to match. This exists +specifically because a stale pin fails `cargo metadata` for the **whole +workspace** the moment a contract bumps a 0.x **minor** version (Cargo's +caret rules treat 0.x minor bumps as breaking) — see PR #79 for a case +where a stale dev-only pin, fixed by hand at the time, silently blocked +`perch-doc-compiler-v0.2.0`'s publish for a day. If you're bumping a +version by hand outside this job (a one-off manual release), still grep for +the crate's name across `crates/*/Cargo.toml` and bump every match — the +automation only runs inside `release-pr`. Once a contract's tag (`-v`) exists, `detect-releases` will never retry that version even if the build later succeeds after a